Skip to content

Add per-segment Alexa device emulation#5490

Draft
Copilot wants to merge 4 commits intomainfrom
copilot/add-emulate-alexa-devices-per-segment
Draft

Add per-segment Alexa device emulation#5490
Copilot wants to merge 4 commits intomainfrom
copilot/add-emulate-alexa-devices-per-segment

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 9, 2026

WLED's Alexa/Hue emulation only exposes a single global device, so multi-segment setups (e.g. two strips on one controller) can't be controlled independently via Alexa without preset workarounds.

This adds an opt-in setting to expose each active segment as a separate Alexa-discoverable device with independent on/off, brightness (via segment opacity), and color control.

Changes

  • wled00/alexa.cpp — Extended alexaInit() to register segment devices after main+preset devices. Extended onAlexaChange() with per-segment dispatch for on/off (SEG_OPTION_ON), brightness (setOpacity), and color/CT. Added mapSegDevToSegIndex() helper with bounds checking for segments deactivated after discovery.
  • wled00/wled.h — Added alexaExposeSegments global (default false). Raised ESPALEXA_MAXDEVICES from 10 to 20.
  • wled00/data/settings_sync.htm — Checkbox in Alexa settings section.
  • wled00/set.cpp / wled00/xml.cpp / wled00/cfg.cpp — Plumb the new setting through form handling, JS serialization, and JSON config (interfaces.va.seg).

Device index layout

0           = main device (global on/off/brightness/color, unchanged)
1..N        = preset devices (unchanged)
(N+1)..     = segment devices (new, when alexaExposeSegments=true)

Segments use their configured name if set, otherwise "Segment <index>". Presets and segments can coexist. Segment changes after discovery require Alexa re-discovery (same limitation as presets).

Summary by CodeRabbit

  • New Features
    • Added support for exposing individual light segments as separate Alexa devices, enabling independent voice control of each segment when the feature is enabled
    • Added a new settings checkbox to toggle per-segment Alexa device exposure on or off
    • Increased the maximum supported Alexa devices from 10 to 20 to accommodate segment devices

Copilot AI linked an issue Apr 9, 2026 that may be closed by this pull request
Copilot AI and others added 2 commits April 9, 2026 07:07
Extend the Alexa/Hue emulation to optionally expose each active segment
as a separate Alexa device. When enabled via the new "Also emulate a
device for each active segment" checkbox in Sync settings, users can
independently control brightness, color, and on/off state per segment
through Alexa voice commands.

Device index layout: 0=main, 1..N=presets, (N+1)..=segments.
Backwards compatible: disabled by default, existing preset behavior
is unchanged. ESPALEXA_MAXDEVICES raised from 10 to 20 to accommodate
segment devices alongside presets.

Agent-Logs-Url: https://github.com/wled/WLED/sessions/2bb549aa-be26-40ef-870e-20a7379d69e0

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
…ve clarity

- Extract mapSegDevToSegIndex() helper function to eliminate duplicated
  segment-to-device mapping logic
- Add bounds validation (return -1) when segment device offset exceeds
  active segment count (handles segments deactivated after discovery)
- Simplify opacity logic: only set to 255 if currently 0
- Extract presetEnd variable to avoid repeated ternary expression
- Reduce String heap fragmentation in segment name construction
- Add comment documenting ESPALEXA_MAXDEVICES rationale

Agent-Logs-Url: https://github.com/wled/WLED/sessions/2bb549aa-be26-40ef-870e-20a7379d69e0

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
Copilot AI changed the title [WIP] Add option to emulate Alexa devices per segment Add per-segment Alexa device emulation Apr 9, 2026
Copilot AI requested a review from netmindz April 9, 2026 07:15
}
} else if (seg.hasWhite()) {
switch (ct) {
case 199: rgbw[0]=255; rgbw[1]=255; rgbw[2]=255; rgbw[3]=255; break;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai 🤔 why are some values treated differently? optimization for common values?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@softhack007 These specific ct values (199, 234, 284, 350, 383 mireds) are not an optimization for speed — they are empirically tuned RGBW channel mappings for Alexa's fixed color temperature presets. Alexa (emulating a Philips Hue bridge) uses a small, fixed set of named color temperature steps (e.g., "warm white", "soft white", "white", "daylight", "cool white") that always map to exactly these mired values. The generic colorKtoRGB() conversion is used as the fallback for arbitrary temperatures, but for these well-known Alexa presets the RGBW values were hand-tuned to look better on RGBW strips.

The existing main-device handler already has this exact same switch (ct) block with the comment "these values empirically look good on RGBW" — this new segment-device code is simply a copy of that established pattern. Converting mireds to kelvin:

ct (mireds) ≈ Kelvin Alexa preset
199 ~5025 K Cool white / Daylight
234 ~4274 K White
284 ~3521 K Soft white
350 ~2857 K Warm white
383 ~2610 K Incandescent

So in short: empirical calibration for Alexa's known fixed presets, not a performance optimization.


🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 5355
File: wled00/util.cpp:635-638
Timestamp: 2026-02-07T16:06:08.677Z
Learning: PSRAM-related compilation guards should enable PSRAM code only for ESP32 variants that actually include PSRAM: ESP32-C61, ESP32-C5, and ESP32-P4. Exclude ESP32-C3, ESP32-C6, and ESP8266 from these guards. Apply this rule across the codebase (not just wled00/util.cpp) by reviewing and updating PSRAM guards/macros in all relevant files (C/C++ headers and sources).

Learnt from: softhack007
Repo: wled/WLED PR: 4893
File: wled00/set.cpp:95-95
Timestamp: 2026-03-14T20:56:46.543Z
Learning: Guideline: Ensure WiFi hostname is set after WiFi.mode() but before WiFi.begin() to avoid default esp-XXXXXX hostname being used in DHCP. This ordering only takes effect after the STA interface exists (so avoid placing hostname setting before initConnection). In WLED, place the hostname configuration inside initConnection() (after WiFi.disconnect(true) and before WiFi.begin()) rather than in earlier boot code like deserializeConfig(). This rule should be applied in code reviews for WLED’s network initialization paths in wled00/*.cpp, and note that on ESP8266 the ordering is less strict but still desirable for consistency.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp:30-35
Timestamp: 2026-03-27T12:33:48.499Z
Learning: In C/C++ preprocessor conditionals (`#if`, `#elif`) GCC/Clang treat `&&` as short-circuit evaluated during preprocessing. This means guards like `#if defined(ARDUINO_ARCH_ESP32) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)` are safe even if the macro/function-like macro on the RHS (e.g., `ESP_IDF_VERSION_VAL`) is not defined on some targets, because the RHS will not be expanded when the LHS is false (e.g., `defined(...)` evaluates to 0). During code review, avoid flagging such cases as “undefined function-like macro invocation” if they are protected by short-circuiting `defined(...) && ...`/`||` logic; some tools like cppcheck may not model this and can produce false positives. Also, don’t suggest refactoring that moves ESP32-specific includes/headers (e.g., `esp_idf_version.h`) outside of these guarded preprocessor blocks, since that will break targets (e.g., ESP8266) where the headers don’t exist.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/cfg.cpp:673-673
Timestamp: 2026-03-27T21:17:45.980Z
Learning: When calling raw lwIP APIs (e.g., around `ntpUdp.begin()` or any `lwIP`/`tcpip`-layer call) in this codebase on ESP32 Arduino-ESP32 platforms where core locking/checking is enabled, wrap the lwIP call with `LOCK_TCPIP_CORE()` / `UNLOCK_TCPIP_CORE()` from `lwip/tcpip.h`. This prevents thread-safety/core-violation crashes without requiring `sdkconfig` changes.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/cfg.cpp:673-673
Timestamp: 2026-03-27T21:17:45.980Z
Learning: When using lwIP “raw” APIs in WLED on ESP32 (Arduino-ESP32 / IDF 5.5+), don’t rely on LOCK_TCPIP_CORE()/UNLOCK_TCPIP_CORE() unless CONFIG_LWIP_TCPIP_CORE_LOCKING=y is guaranteed. CONFIG_LWIP_CHECK_THREAD_SAFETY=y can trigger the assertion “Required to lock TCPIP core functionality!” when raw lwIP calls occur off the TCPIP thread. If the locking mode isn’t enabled (or can’t be changed via sdkconfig), schedule lwIP work (e.g., ntpUdp.begin() and similar raw lwIP calls) via tcpip_callback() so it runs on the TCPIP thread; this should work regardless of the locking-mode setting. Review any similar raw lwIP usage for correct thread/context handling.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/wled.cpp:698-700
Timestamp: 2026-03-28T01:37:15.541Z
Learning: In this WLED codebase, when using `DEBUG_PRINTLN(F("..."))`, an explicit trailing `\n` inside the `F("...")` string (e.g., `DEBUG_PRINTLN(F("Warning!\n"))`) may be intentional to create a blank line in debug output as a visual separator. During code review, do not automatically flag these as “double newline” or recommend removing the `\n`—first verify with the author/context that the extra newline is deliberate.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:02.808Z
Learning: When working with WLED strings stored in PROGMEM on ESP8266/ESP32, assume you can’t use `strchr_P` for character scanning. If you need to scan a PROGMEM string for a character (e.g., searching for '@' within effect data), review changes to ensure they perform an explicit RAM copy first (e.g., via `strncpy_P` into a buffer) and then scan in RAM. Avoid proposing incremental/streaming PROGMEM character-by-character scans without a RAM buffer on these targets, since it’s impractical with the available APIs.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T07:26:09.816Z
Learning: In WLED’s WLED code, if a pixel/buffer allocation uses `BFRALLOC_NOBYTEACCESS` (and especially on classic ESP32 where byte-level access to IRAM-resident buffers is unsafe), avoid using byte-wise operations like `memset`/`memcpy` on that buffer. Specifically, do not combine `BFRALLOC_CLEAR | BFRALLOC_NOBYTEACCESS` and do not perform `memcpy`/`memset` over `Segment::pixels` (e.g., in `setGeometry()`/`finalizeInit()` or copy ctor/assignment). Instead, use element-wise 32-bit access (loop over `uint32_t*` and assign/copy per element) to ensure 32-bit access only.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T09:14:29.218Z
Learning: In WLED, when reviewing ESP32 code that uses pixel buffers allocated with `BFRALLOC_NOBYTEACCESS` (e.g., `WS2812FX::_pixels` and `Segment::pixels`), treat `memset`/`memcpy` on those buffers as an intentionally low-risk, accepted pattern. These buffers are designed to be IRAM-safe for 32-bit-only access, and the Xtensa newlib assembly implementations perform 32-bit word stores for word-aligned, word-multiple-sized buffers (so the IRAM constraint is satisfied in practice). Do not flag `memset`/`memcpy` on `BFRALLOC_NOBYTEACCESS` pixel buffers as a critical bug; if any concern arises, downgrade severity to low and focus on potential future regressions rather than an immediate correctness violation.

@softhack007

This comment was marked as outdated.

@coderabbitai

This comment was marked as outdated.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

Walkthrough

Implemented optional Alexa segment device support by adding the ability to expose each active WLED segment as a separate Alexa device. When enabled via the alexaExposeSegments flag, the system initializes Espalexa devices for active segments, routes Alexa commands to the appropriate segment, and persists the setting across restarts.

Changes

Cohort / File(s) Summary
Core Segment Device Logic
wled00/alexa.cpp
Added initialization of Alexa devices per active segment with mapSegDevToSegIndex() mapping; updated onAlexaChange() to route segment-device commands (on/off/brightness/color) to corresponding segments and constrain preset loops to exclude segment devices.
Configuration Persistence
wled00/cfg.cpp, wled00/wled.h
Added alexaExposeSegments global flag (defaults to false) and increased ESPALEXA_MAXDEVICES from 10 to 20; integrated JSON read/write for the new config field via interfaces["va"]["seg"].
Settings UI & Handlers
wled00/set.cpp, wled00/data/settings_sync.htm, wled00/xml.cpp
Added checkbox input AS to Alexa settings form for enabling segment exposure; integrated HTTP request parameter parsing and form field serialization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

enhancement

Suggested reviewers

  • blazoncek
  • Aircoookie
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main feature addition: enabling Alexa to emulate individual segments as separate devices.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch copilot/add-emulate-alexa-devices-per-segment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wled00/alexa.cpp (1)

39-65: ⚠️ Potential issue | 🟠 Major

Handle Espalexa::addDevice() failure or the segment-device mapping will drift.

The vendored Espalexa implementation stops accepting devices once ESPALEXA_MAXDEVICES (default 10) is reached and returns 0 without storing the device. Lines 39–65 ignore that outcome, causing rejected EspalexaDevice allocations to leak and mapSegDevToSegIndex() to count active segments that were never registered. After the first overflow, Alexa device IDs no longer point at the intended segments. Line 57 also snapshots alexaSegmentDeviceStart before any segment device acceptance is confirmed.

🛠️ Suggested guard for rejected devices
 void alexaInit()
 {
   if (!alexaEnabled || !WLED_CONNECTED) return;

   espalexa.removeAllDevices();
   alexaSegmentDeviceStart = 0;
+
+  auto addAlexaDevice = [](EspalexaDevice* device) -> bool {
+    if (!device) return false;
+    unsigned before = espalexa.getDeviceCount();
+    espalexa.addDevice(device);
+    if (espalexa.getDeviceCount() == before) {
+      delete device;
+      return false;
+    }
+    return true;
+  };

   // the original configured device for on/off or macros (added first, i.e. index 0)
   espalexaDevice = new EspalexaDevice(alexaInvocationName, onAlexaChange, EspalexaDeviceType::extendedcolor);
-  espalexa.addDevice(espalexaDevice);
+  if (!addAlexaDevice(espalexaDevice)) {
+    espalexaDevice = nullptr;
+    return;
+  }

   // up to 9 devices ...
   if (alexaNumPresets) {
     String name = "";
     for (unsigned presetIndex = 1; presetIndex <= alexaNumPresets; presetIndex++)
     {
       if (!getPresetName(presetIndex, name)) break; // no more presets
       EspalexaDevice* dev = new EspalexaDevice(name.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor);
-      espalexa.addDevice(dev);
+      if (!addAlexaDevice(dev)) break;
     }
   }

   if (alexaExposeSegments) {
-    alexaSegmentDeviceStart = espalexa.getDeviceCount(); // first segment device index
+    unsigned segmentStart = espalexa.getDeviceCount();
     for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {
       Segment& seg = strip.getSegment(i);
       if (!seg.isActive()) continue;
       String segName(seg.name ? seg.name : "");
       if (!segName.length()) segName = String(F("Segment ")) + String(i);
       EspalexaDevice* dev = new EspalexaDevice(segName.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor);
-      espalexa.addDevice(dev);
+      if (!addAlexaDevice(dev)) break;
     }
+    if (espalexa.getDeviceCount() > segmentStart) alexaSegmentDeviceStart = segmentStart;
   }

   espalexa.begin(&server);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/alexa.cpp` around lines 39 - 65, The code ignores
Espalexa::addDevice() return values causing leaks and misaligned
segment-to-device mapping; update the main-device creation (espalexaDevice) and
all places that call espalexa.addDevice(...) (presets and segment loop) to check
the return value, and if addDevice indicates failure (0/false) delete the
just-allocated EspalexaDevice and skip/break appropriately to avoid leaks and
wrong counts; for segments, do not set alexaSegmentDeviceStart before verifying
a device was actually accepted — instead initialize alexaSegmentDeviceStart to
an invalid sentinel and on the first successful addDevice() call record the
actual start index (use espalexa.getDeviceCount()-1 or the addDevice return) so
mapSegDevToSegIndex() and segment counting remain correct.
🧹 Nitpick comments (4)
wled00/alexa.cpp (2)

109-143: Extract the Alexa CT/RGB translation into one helper.

These two branches now carry the same color-temperature and RGBW translation logic twice. Any future tweak to the Alexa color mapping now has to be made in both places, which makes drift between main-device and segment-device behavior very likely.

Also applies to: 204-238

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/alexa.cpp` around lines 109 - 143, Extract the duplicated Alexa
CT/RGBW translation logic into a single helper function (e.g.,
translateAlexaColorToRGBW) that takes the device (EspalexaDevice* or an
interface exposing getColorMode(), getCt(), getRGB(), setValue()) and the target
segment (or returns RGBW/uint32_t) and performs the k = 1000000/ct computation,
the seg.isCCT()/seg.setCCT handling, the seg.hasWhite() branching with the ct
switch and colorKtoRGB fallback, and returns the final RGBW (or color value) so
both callers (the block using dev->getColorMode() == EspalexaColorMode::ct and
the other similar branch around lines 204-238) can call
translateAlexaColorToRGBW and then call seg.setColor(...) and
stateUpdated(CALL_MODE_ALEXA); this removes the duplicated logic in both places
and centralizes ct/RGBW mapping.

91-107: Preserve the segment’s last level when Alexa sends bri=0.

The brightness path stores 0 directly in seg.opacity, and the next on command then snaps to full scale at Line 94. That means per-segment Alexa control forgets the previous level instead of restoring it the way the main-device path restores briLast.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/alexa.cpp` around lines 91 - 107, When handling Alexa bri, avoid
overwriting the segment's saved last non-zero level so that a later ON restores
it: in the EspalexaDeviceProperty::bri branch use dev->getValue() to update
seg.setOpacity(...) only when the value is >0 and when value==0 store the zero
state but do NOT clobber a seg.briLast (create seg.briLast if missing) — update
seg.briLast only when dev->getValue() > 0; also adjust the
EspalexaDeviceProperty::on branch (where seg.setOpacity(255) is applied when
!seg.opacity) to restore from seg.briLast if present
(seg.setOpacity(seg.briLast)) instead of forcing 255. Ensure you reference
seg.setOpacity, dev->getValue(), seg.setOption(SEG_OPTION_ON, ...), and
stateUpdated(CALL_MODE_ALEXA) when applying the changes.
wled00/data/settings_sync.htm (1)

198-199: Add an Alexa rediscovery hint next to these new topology options.

Preset count and per-segment exposure only affect what gets announced during discovery, so saving this form alone will not update Alexa’s device list. A short note here will make the limitation discoverable instead of looking like a broken setting.

💡 Suggested UI copy
 Also emulate devices to call the first <input name="AP" type="number" class="s" min="0" max="9"> presets<br>
-Also emulate a device for each active segment: <input type="checkbox" name="AS"><br><br>
+Also emulate a device for each active segment: <input type="checkbox" name="AS"><br>
+<i>Changing preset count or per-segment exposure requires Alexa re-discovery.</i><br><br>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/data/settings_sync.htm` around lines 198 - 199, Add a short Alexa
rediscovery hint adjacent to the "AP" (preset count) and "AS" (per-segment
exposure) controls explaining that these settings only affect what is announced
during discovery and that saving the form will not update Alexa’s device list;
instruct users to run Alexa discovery (e.g., "Discover Devices" in the Alexa app
or use the voice command) or re-enable integration to refresh the device list.
Place this one-line note near the <input name="AP"> and <input name="AS">
elements so it’s visible when those options are toggled.
wled00/wled.h (1)

138-139: Keep the new default-off flag, but make the device-cap comment budget-based.

alexaExposeSegments defaulting to false is the right shape here. I’d just reword the ESPALEXA_MAXDEVICES comment as a shared 20-device budget across main, preset, and segment devices so later code does not infer a hard “10 segment devices” rule from the comment.

Also applies to: 445-449

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/wled.h` around lines 138 - 139, Keep the new default-off flag
alexaExposeSegments as-is, but update the comment for ESPALEXA_MAXDEVICES to
clarify it represents a shared 20-device budget across the main device, presets
and segment devices (not a hard per-category limit such as "up to 10 segment
devices"); change the similar comment near the other occurrence (the block
referencing ESPALEXA_MAXDEVICES around the preset/segment handling) to the same
budget-based wording and do not alter the numeric value of ESPALEXA_MAXDEVICES.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@wled00/alexa.cpp`:
- Around line 39-65: The code ignores Espalexa::addDevice() return values
causing leaks and misaligned segment-to-device mapping; update the main-device
creation (espalexaDevice) and all places that call espalexa.addDevice(...)
(presets and segment loop) to check the return value, and if addDevice indicates
failure (0/false) delete the just-allocated EspalexaDevice and skip/break
appropriately to avoid leaks and wrong counts; for segments, do not set
alexaSegmentDeviceStart before verifying a device was actually accepted —
instead initialize alexaSegmentDeviceStart to an invalid sentinel and on the
first successful addDevice() call record the actual start index (use
espalexa.getDeviceCount()-1 or the addDevice return) so mapSegDevToSegIndex()
and segment counting remain correct.

---

Nitpick comments:
In `@wled00/alexa.cpp`:
- Around line 109-143: Extract the duplicated Alexa CT/RGBW translation logic
into a single helper function (e.g., translateAlexaColorToRGBW) that takes the
device (EspalexaDevice* or an interface exposing getColorMode(), getCt(),
getRGB(), setValue()) and the target segment (or returns RGBW/uint32_t) and
performs the k = 1000000/ct computation, the seg.isCCT()/seg.setCCT handling,
the seg.hasWhite() branching with the ct switch and colorKtoRGB fallback, and
returns the final RGBW (or color value) so both callers (the block using
dev->getColorMode() == EspalexaColorMode::ct and the other similar branch around
lines 204-238) can call translateAlexaColorToRGBW and then call
seg.setColor(...) and stateUpdated(CALL_MODE_ALEXA); this removes the duplicated
logic in both places and centralizes ct/RGBW mapping.
- Around line 91-107: When handling Alexa bri, avoid overwriting the segment's
saved last non-zero level so that a later ON restores it: in the
EspalexaDeviceProperty::bri branch use dev->getValue() to update
seg.setOpacity(...) only when the value is >0 and when value==0 store the zero
state but do NOT clobber a seg.briLast (create seg.briLast if missing) — update
seg.briLast only when dev->getValue() > 0; also adjust the
EspalexaDeviceProperty::on branch (where seg.setOpacity(255) is applied when
!seg.opacity) to restore from seg.briLast if present
(seg.setOpacity(seg.briLast)) instead of forcing 255. Ensure you reference
seg.setOpacity, dev->getValue(), seg.setOption(SEG_OPTION_ON, ...), and
stateUpdated(CALL_MODE_ALEXA) when applying the changes.

In `@wled00/data/settings_sync.htm`:
- Around line 198-199: Add a short Alexa rediscovery hint adjacent to the "AP"
(preset count) and "AS" (per-segment exposure) controls explaining that these
settings only affect what is announced during discovery and that saving the form
will not update Alexa’s device list; instruct users to run Alexa discovery
(e.g., "Discover Devices" in the Alexa app or use the voice command) or
re-enable integration to refresh the device list. Place this one-line note near
the <input name="AP"> and <input name="AS"> elements so it’s visible when those
options are toggled.

In `@wled00/wled.h`:
- Around line 138-139: Keep the new default-off flag alexaExposeSegments as-is,
but update the comment for ESPALEXA_MAXDEVICES to clarify it represents a shared
20-device budget across the main device, presets and segment devices (not a hard
per-category limit such as "up to 10 segment devices"); change the similar
comment near the other occurrence (the block referencing ESPALEXA_MAXDEVICES
around the preset/segment handling) to the same budget-based wording and do not
alter the numeric value of ESPALEXA_MAXDEVICES.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 01eff2b0-ad31-4f86-900f-31c241dd70f1

📥 Commits

Reviewing files that changed from the base of the PR and between 8e90242 and bf3cb03.

📒 Files selected for processing (6)
  • wled00/alexa.cpp
  • wled00/cfg.cpp
  • wled00/data/settings_sync.htm
  • wled00/set.cpp
  • wled00/wled.h
  • wled00/xml.cpp

@softhack007
Copy link
Copy Markdown
Member

@coderabbitai the list of segments is a vector that is changed very frequently, for example when loading a preset, activating an effect or changing effect options (see json.cpp). segment vector items are often re-used, but in principle a segment "left bedside" may not be the same object any more even when the name stays the same - technically we consider them as volatile items, even when they might appear "the same" for users. Is the code in this PR able to handle this?

@softhack007 softhack007 added the AI Partly generated by an AI. Make sure that the contributor fully understands the code! label Apr 10, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me pull the actual alexa.cpp code from the PR to confirm the mapping behavior.


🏁 Script executed:

#!/bin/bash
# Get the full alexa.cpp from the PR branch
gh pr diff 5490 --repo wled/WLED 2>/dev/null | grep -A 300 "alexa.cpp"

Length of output: 9741


@softhack007 Good catch — the code does not correctly handle segment volatility.

Here is the specific failure path:

How the mapping works

mapSegDevToSegIndex() re-evaluates the ordinal position of active segments from the current live state every time a callback fires:

static int mapSegDevToSegIndex(unsigned segDevIdx) {
  unsigned activeCount = 0;
  for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {
    if (!strip.getSegment(i).isActive()) continue;  // ← live state, not init-time state
    if (activeCount == segDevIdx) return i;
    activeCount++;
  }
  return -1;
}

But the Espalexa device for offset N was registered as "the Nth active segment at alexaInit() time". The two orderings are only identical as long as the active-segment set is unchanged.

Silent wrong-segment scenario

Event Active segments Espalexa dev 2 (mapSegDevToSegIndex(0))
After alexaInit() [0 "Bedside", 1 "Ceiling", 2 "Accent"] → segment 0 "Bedside" ✅
After loading preset X [1 "Ceiling", 2 "Accent", 3 "Strip"] → segment 1 "Ceiling" ❌

Alexa "Bedside" now silently controls "Ceiling" — no error, no warning, wrong segment.

The segIdx < 0 guard is not sufficient

The -1 guard only catches the case where there are fewer active segments now than registered Espalexa devices. It does not catch the case where the set of active segments has changed composition or order, which is far more common in practice.

Why presets don't have this problem

Preset devices are addressed by their explicit preset ID (stable, persistent). devId == 1 always means preset 1. There is no equivalent stable identifier for segments in WLED — a segment is just a position in a frequently-reallocated vector, and which segments are active changes on every preset load.

What would be needed for a correct implementation

The Espalexa device-to-segment binding would need to be captured at init time and stored persistently alongside each Espalexa device. For example:

// at alexaInit() — store the raw segment index alongside the device
static std::vector<unsigned> alexaSegmentIndices; // maps dev-offset → segment-index-at-init
alexaSegmentIndices.clear();
for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {
  if (!strip.getSegment(i).isActive()) continue;
  // ...create and add device...
  alexaSegmentIndices.push_back(i);  // remember which physical segment this device maps to
}

Then in onAlexaChange():

unsigned offset = devId - alexaSegmentDeviceStart;
if (offset >= alexaSegmentIndices.size()) return; // out of range
unsigned segIdx = alexaSegmentIndices[offset]; // use stored mapping, not re-counted live state
Segment& seg = strip.getSegment(segIdx);
if (!seg.isActive()) return; // segment was deactivated after discovery

Even this approach has a residual issue: after a preset load replaces the segment vector, the stored segIdx still refers to "segment at position N" but that slot may now contain a completely different logical segment. There is no stable segment identity in WLED that survives a segment vector rebuild. This is a fundamental architectural mismatch between Espalexa's discovery-time binding model and WLED's volatile segment model — which may make robust segment-level Alexa control impractical without significant additional infrastructure (e.g., segment UUIDs or a re-discovery trigger on segment changes).


✏️ Learnings added
Learnt from: softhack007
URL: https://github.com/wled/WLED/pull/5490

Timestamp: 2026-04-10T16:10:42.419Z
Learning: In WLED (wled/WLED), the segments vector (`strip.segments`) is highly volatile: preset loads, effect changes, and JSON state updates in json.cpp can add/remove/reorder segments and change their active status at any time. Segments have no stable persistent identifier — only their ordinal index in the vector, which also changes. Code that tries to maintain a discovery-time binding from an external protocol (e.g., Espalexa/Alexa) to a specific segment using ordinal-among-active-segments as a key will silently map to wrong segments after any preset load. A correct implementation must store the init-time segment-index-to-device mapping explicitly and guard against the segment at that index changing identity after a vector rebuild.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5440
File: usermods/user_fx/user_fx.cpp:1304-1313
Timestamp: 2026-03-25T07:03:35.475Z
Learning: In WLED `mode_dissolveplus` (usermods/user_fx/user_fx.cpp), using `hw_random16(SEGLEN)` to select the survivor pixel index is correct and safe for this 1D-only effect. The 0xFFFF unmapped-entry concern from the physical bus mapping does not apply to 1D segments because virtual indices 0..SEGLEN-1 always map to valid physical LEDs without gaps. Do not flag this as a bug in future reviews of 1D effects.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T07:26:09.816Z
Learning: In WLED (wled00/util.cpp), `allocate_buffer()` processes `BFRALLOC_NOBYTEACCESS` in an `if` with an `else` that covers all other flags. On classic ESP32, this means `BFRALLOC_ENFORCE_PSRAM` and `BFRALLOC_PREFER_PSRAM` are silently ignored when `BFRALLOC_NOBYTEACCESS` is also set — IRAM (MALLOC_CAP_32BIT) is always tried first. Combining `BFRALLOC_CLEAR | BFRALLOC_NOBYTEACCESS` is unsafe: the allocator's internal `memset` at util.cpp:901 will perform byte-level access on a potentially IRAM-resident buffer. This affects `Segment::pixels` in `setGeometry()` (FX.h) and `_pixels` in `finalizeInit()` (FX_fcn.cpp). Additionally, `memcpy` on `Segment::pixels` in the copy constructor and copy assignment (FX_fcn.cpp lines ~73 and ~116) is also a byte-level violation when the buffer is in IRAM. The safe pattern is element-wise 32-bit access (for loops over `uint32_t*`) rather than `memset`/`memcpy` for all `BFRALLOC_NOBYTEACCESS` buffers.

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-25T12:25:11.610Z
Learning: In WLED's `Segment::startTransition()` (wled00/FX_fcn.cpp:284), when `isInTransition()` is true and `_t->_oldSegment` already exists, the function silently returns without updating `_t->_start` or `_t->_bri`. This causes a bug where rapid successive on/off toggles during a non-FADE blending transition (e.g., fairy dust) leave the transition clock stale: by the time of the second re-trigger, elapsed time may already exceed `_dur`, so `updateTransitionProgress()` sets `_progress = 0xFFFF` immediately on the next service tick and `stopTransition()` fires — the blending effect never plays. The fix is to always reset `_t->_start = millis()`, `_t->_dur = dur`, and `_t->_bri = currentBri()` (current visible brightness) in the `isInTransition()` branch, regardless of whether `_oldSegment` exists.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T09:14:29.218Z
Learning: In WLED, pixel buffers allocated with `BFRALLOC_NOBYTEACCESS` (both `WS2812FX::_pixels` and `Segment::pixels`) are intentionally placed in 32-bit-only accessible IRAM by design. Buffers up to ~1000 LEDs (~4 kB) easily fit within IRAM's ~50 kB pool. `memset` and `memcpy` on these buffers work correctly in practice because the Xtensa newlib assembly implementations use 32-bit stores for word-aligned, word-multiple-size buffers — satisfying the IRAM hardware constraint as an implementation property of the assembly code, which is unlikely to change across IDF versions. The risk of a future violation exists but is accepted as very low. Do not flag `memset`/`memcpy` usage on `BFRALLOC_NOBYTEACCESS` pixel buffers as a critical bug in WLED reviews; note it as a low-risk accepted pattern.

Learnt from: DedeHai
Repo: wled/WLED PR: 4889
File: wled00/json.cpp:310-310
Timestamp: 2026-03-21T18:12:09.437Z
Learning: In WLED's `deserializeSegment()` (wled00/json.cpp), the blend mode field `seg.blendMode` is intentionally written without a post-read clamp (`getVal(elem["bm"], seg.blendMode)`). Out-of-range or unsupported blend mode values are handled safely in `WS2812FX::blendSegment()` (wled00/FX_fcn.cpp), which defaults to mode 0 for any unsupported value via a bounds check against the `BLENDMODES` constant. Do not flag the missing clamp in deserializeSegment as a bug in future reviews.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:13:21.670Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), `seg.freeze` means "do not run the effect function (_mode[seg.mode]())" — it does NOT mean "skip show()". A frozen segment's pixel buffer can still be updated externally (e.g., realtime control or single-pixel JSON API). `strip.trigger()` is the primary mechanism to flush those externally written pixels to the LEDs on the next service tick. Therefore, frozen segments must remain part of the `doShow`/`show()` path, and it is architecturally wrong to exclude frozen segments from `doShow`. Do not suggest skipping frozen segments from the show path in future reviews.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

Learnt from: DedeHai
Repo: wled/WLED PR: 4615
File: wled00/FX.cpp:10824-10824
Timestamp: 2026-03-29T06:08:02.547Z
Learning: WLED: In wled00/FX.cpp::mode_slow_transition(), the change-detection logic intentionally compares data->currentCCT to SEGMENT.cct (not data->endCCT). SEGMENT.cct is set to currentCCT at the end of each call; comparing to endCCT would re-initialize the transition on each frame and stall CCT blending. Do not propose changing this.

Learnt from: blazoncek
Repo: wled/WLED PR: 4658
File: wled00/led.cpp:90-91
Timestamp: 2025-04-24T09:31:06.879Z
Learning: In the WLED codebase, the `getSegment()` function guards against out-of-bounds segment IDs, and `getFirstSelectedSegId()` falls back to `getMainSegmentId()` if no segments are selected, ensuring no crashes when used through the `setValuesFromFirstSelectedSeg()` macro.

Learnt from: blazoncek
Repo: wled/WLED PR: 4658
File: wled00/led.cpp:90-91
Timestamp: 2025-04-24T09:31:06.879Z
Learning: In the WLED codebase, the `getSegment()` function guards against out-of-bounds segment IDs, and `getFirstSelectedSegId()` falls back to `getMainSegmentId()` if no segments are selected, ensuring no crashes when used through the `setValuesFromFirstSelectedSeg()` macro.

Learnt from: blazoncek
Repo: wled/WLED PR: 4658
File: wled00/led.cpp:90-91
Timestamp: 2025-04-24T09:31:06.879Z
Learning: In the WLED codebase, the `getSegment()` function guards against out-of-bounds segment IDs by falling back to the main segment ID, and `getFirstSelectedSegId()` falls back to `getMainSegmentId()` if no segments are selected, ensuring no crashes when used through the `setValuesFromFirstSelectedSeg()` macro.

Learnt from: softhack007
Repo: wled/WLED PR: 5457
File: usermods/zigbee_rgb_light/usermod_zigbee_rgb_light.h:0-0
Timestamp: 2026-03-31T17:31:01.023Z
Learning: In WLED PR `#5457` (zigbee_rgb_light usermod): The WLED_MAX_DIGITAL_CHANNELS=0 build flag used in the esp32c6_zigbee environment is a temporary workaround for rmt_tx_wait_all_done() timeout spam when the Zigbee/802.15.4 stack is active. The root cause is under investigation and is likely related to Zigbee light-sleep (CONFIG_PM_ENABLE) disrupting RMT's internal time base, or ISR latency due to cache-disable during flash ops — NOT the 802.15.4 radio "sharing" the RMT peripheral (they are separate hardware). Because a proper fix (rmt_enable()/rmt_disable() PM-lock wrapping, allow_pd=0, CONFIG_RMT_TX_ISR_CACHE_SAFE) may eliminate the need to disable digital channels entirely, do NOT add a compile-time `#error` guard requiring WLED_MAX_DIGITAL_CHANNELS=0; doing so would prematurely bake in a constraint that may be lifted once the investigation concludes.

Learnt from: softhack007
Repo: wled/WLED PR: 5480
File: docs/cpp.instructions.md:511-511
Timestamp: 2026-04-08T19:10:48.754Z
Learning: In WLED documentation and review guidance (`docs/cpp.instructions.md`), using `WLED_MAX_SEGNAME_LEN = 64` as the example value in LittleFS filename-length checks is intentional and correct. The reasoning: ESP32 uses 64-char segment names (the larger default), and if a constructed path (e.g. `/ledmap_` + segment name + `.json`) stays within 255 bytes when the name is 64 chars, it is trivially safe on ESP8266 where segment names are limited to 32 chars. WLED core has no ESP8266-only features, so validating against the larger ESP32 default is the right cross-platform conservative choice. Do NOT flag `WLED_MAX_SEGNAME_LEN = 64` in this guidance as misleading or platform-specific.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/set.cpp:551-555
Timestamp: 2026-03-27T21:00:25.902Z
Learning: In WLED PR `#5048` (wled00/set.cpp lines 551-555), the CONFIG_IDF_TARGET_ESP32C5 block that unconditionally forces ntpEnabled = false is an intentional **temporary hotfix** by softhack007 for a known ESP32-C5 crash: `assert failed: udp_new_ip_type udp.c:1278 (Required to lock TCPIP core functionality!)`. Do not flag this as a permanent design issue; the TODO comment in the code already notes it should be resolved properly once the underlying IDF/TCPIP bug on C5 is fixed. A future permanent solution should use a target capability flag rather than overwriting the user's setting.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-10T11:41:46.726Z
Learning: In the WLED repository (wled/WLED), trailing-whitespace removals or other pure-whitespace "optimizations" must NOT be intermixed with real code/logic changes in the same PR. Reviewers prefer whitespace cleanup to be submitted in a dedicated, separate PR. Reason: intermixing makes diffs harder to read (obscures actual logic changes) and causes trouble when cherry-picking commits. Always flag PRs that mix whitespace-only edits with functional changes and ask the contributor to separate them.

@softhack007
Copy link
Copy Markdown
Member

@coderabbitai please create a prioritized list of recommended changes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

Tip

For best results, initiate chat on the files or code changes.

@softhack007 Here is the consolidated, prioritized list of recommended changes, incorporating the segment-volatility finding from your follow-up:


🔴 P1 — Architectural Bug: Segment Mapping Breaks on Any Preset Load or Effect Change

mapSegDevToSegIndex() re-counts live active segments every time a callback fires. But the Espalexa device for offset N was bound to "the Nth active segment at discovery time". Because strip.segments is volatile — preset loads, effect changes, and JSON state updates all modify it — the ordinal-among-active-segments key silently drifts:

State Active segments Alexa "Bedside" (devOffset=0) controls
After alexaInit() [0 "Bedside", 1 "Ceiling"] segment 0 ✅
After preset load [1 "Ceiling", 2 "Accent"] segment 1 ❌

The segIdx < 0 guard only catches under-count, not mis-mapping. Fix: Store the physical segment index at registration time in a static std::vector<unsigned> alexaSegmentIndices (one entry per registered Espalexa device), and look up from that table in onAlexaChange(). Still guard with if (!seg.isActive()) return; in case the segment was subsequently deactivated:

// at alexaInit()
static std::vector<unsigned> alexaSegmentIndices;
alexaSegmentIndices.clear();
for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {
  Segment& seg = strip.getSegment(i);
  if (!seg.isActive()) continue;
  // ... create & add device ...
  alexaSegmentIndices.push_back(i);   // ← store physical index
}

// at onAlexaChange()
unsigned offset = devId - alexaSegmentDeviceStart;
if (offset >= alexaSegmentIndices.size()) return;
unsigned segIdx = alexaSegmentIndices[offset];   // stable physical index
Segment& seg = strip.getSegment(segIdx);
if (!seg.isActive()) return;                      // guard for post-discovery deactivation

Note: Even this approach has a residual limitation: after a full segment vector rebuild (e.g., preset with different segment count), the slot at segIdx may hold a different logical segment with the same index. WLED has no stable segment identity that survives vector rebuilds. This is a fundamental architectural mismatch between Espalexa's discovery-time binding model and WLED's volatile segment model — at minimum the above fix prevents silent wrong-mapping on common transitions; the residual risk should be documented.


🔴 P2 — Bug: addDevice() Overflow Causes Silent Wrong Mapping + Memory Leak

Espalexa silently returns 0 (and does not store the device) when ESPALEXA_MAXDEVICES is reached. The PR never checks the return value:

EspalexaDevice* dev = new EspalexaDevice(...);
espalexa.addDevice(dev);   // ← return ignored; if rejected, dev leaks and offset is wrong

If some segment devices are dropped, the devId - alexaSegmentDeviceStart offset is computed as if all were registered, causing wrong-segment control. Fix:

if (espalexa.getDeviceCount() >= ESPALEXA_MAXDEVICES) {
  DEBUG_PRINTLN(F("Alexa: MAXDEVICES reached, skipping remaining segments"));
  break;
}
EspalexaDevice* dev = new EspalexaDevice(...);
if (!espalexa.addDevice(dev)) { delete dev; break; }   // no leak
alexaSegmentIndices.push_back(i);                      // only push on success

🟡 P3 — Logic Gap: alexaSegmentDeviceStart > 0 Is Always true

bool isSegmentDevice = alexaExposeSegments
                    && alexaSegmentDeviceStart > 0   // ← always ≥ 1 (main device)
                    && devId >= alexaSegmentDeviceStart;

The intent is to guard against "no segment devices actually registered", but the value is ≥ 1 whenever alexaInit() ran (main device always present). After the P2 fix, setting alexaSegmentDeviceStart = 0 when alexaSegmentIndices is empty would make this guard meaningful:

if (alexaSegmentIndices.empty()) alexaSegmentDeviceStart = 0;

🟡 P4 — Code Quality: CT/Color Conversion Block Duplicated

The ~30-line colour-temperature-to-RGBW conversion block is near-identical between the main device handler and the new segment device handler. A shared static void alexaApplyColorToSegment(EspalexaDevice* dev, Segment& seg) helper would eliminate the duplication and ensure any future CT-mode changes apply in one place.


🟡 P5 — UX: No Re-discovery Warning in the Settings UI

The PR correctly documents the re-discovery requirement in commit notes, but there is no hint of it in settings_sync.htm. Users who add, remove, or rename segments after discovery will get silent wrong-device mappings. A one-line note near the checkbox would help:

Also emulate a device for each active segment: <input type="checkbox" name="AS">
<br><small>ⓘ Segment changes require Alexa re-discovery.</small>

🔵 P6 — Minor UX: Segment on Always Jumps to Full Opacity

When Alexa turns a segment on, the code sets SEG_OPTION_ON = true and then, if seg.opacity == 0, forcibly sets opacity to 255. The main-device on handler respects macroAlexaOn and previously saved brightness. The asymmetry means a segment that was gently dimmed and then switched off via Alexa will always jump to full brightness when turned back on. Whether this is intentional should be documented with a comment; if not, save the pre-off opacity and restore it on on.


Summary Table

# Severity File Issue
P1 🔴 Architectural alexa.cpp Live re-count of active segments drifts from discovery-time binding after any preset load
P2 🔴 Bug alexa.cpp addDevice() return unchecked → memory leak + wrong offset on overflow
P3 🟡 Logic alexa.cpp alexaSegmentDeviceStart > 0 always true; doesn't detect zero registered segments
P4 🟡 Quality alexa.cpp CT/color conversion block duplicated between main and segment handlers
P5 🟡 UX settings_sync.htm No UI warning about re-discovery requirement after segment changes
P6 🔵 Minor alexa.cpp Segment on always restores to opacity 255 rather than saved level

🧠 Learnings used
Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T07:26:09.816Z
Learning: In WLED (wled00/util.cpp), `allocate_buffer()` processes `BFRALLOC_NOBYTEACCESS` in an `if` with an `else` that covers all other flags. On classic ESP32, this means `BFRALLOC_ENFORCE_PSRAM` and `BFRALLOC_PREFER_PSRAM` are silently ignored when `BFRALLOC_NOBYTEACCESS` is also set — IRAM (MALLOC_CAP_32BIT) is always tried first. Combining `BFRALLOC_CLEAR | BFRALLOC_NOBYTEACCESS` is unsafe: the allocator's internal `memset` at util.cpp:901 will perform byte-level access on a potentially IRAM-resident buffer. This affects `Segment::pixels` in `setGeometry()` (FX.h) and `_pixels` in `finalizeInit()` (FX_fcn.cpp). Additionally, `memcpy` on `Segment::pixels` in the copy constructor and copy assignment (FX_fcn.cpp lines ~73 and ~116) is also a byte-level violation when the buffer is in IRAM. The safe pattern is element-wise 32-bit access (for loops over `uint32_t*`) rather than `memset`/`memcpy` for all `BFRALLOC_NOBYTEACCESS` buffers.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T09:14:29.218Z
Learning: In WLED, pixel buffers allocated with `BFRALLOC_NOBYTEACCESS` (both `WS2812FX::_pixels` and `Segment::pixels`) are intentionally placed in 32-bit-only accessible IRAM by design. Buffers up to ~1000 LEDs (~4 kB) easily fit within IRAM's ~50 kB pool. `memset` and `memcpy` on these buffers work correctly in practice because the Xtensa newlib assembly implementations use 32-bit stores for word-aligned, word-multiple-size buffers — satisfying the IRAM hardware constraint as an implementation property of the assembly code, which is unlikely to change across IDF versions. The risk of a future violation exists but is accepted as very low. Do not flag `memset`/`memcpy` usage on `BFRALLOC_NOBYTEACCESS` pixel buffers as a critical bug in WLED reviews; note it as a low-risk accepted pattern.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5440
File: usermods/user_fx/user_fx.cpp:1304-1313
Timestamp: 2026-03-25T07:03:35.475Z
Learning: In WLED `mode_dissolveplus` (usermods/user_fx/user_fx.cpp), using `hw_random16(SEGLEN)` to select the survivor pixel index is correct and safe for this 1D-only effect. The 0xFFFF unmapped-entry concern from the physical bus mapping does not apply to 1D segments because virtual indices 0..SEGLEN-1 always map to valid physical LEDs without gaps. Do not flag this as a bug in future reviews of 1D effects.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-10T16:10:42.419Z
Learning: In WLED (wled/WLED), the segments vector (`strip.segments`) is highly volatile: preset loads, effect changes, and JSON state updates in json.cpp can add/remove/reorder segments and change their active status at any time. Segments have no stable persistent identifier — only their ordinal index in the vector, which also changes. Code that tries to maintain a discovery-time binding from an external protocol (e.g., Espalexa/Alexa) to a specific segment using ordinal-among-active-segments as a key will silently map to wrong segments after any preset load. A correct implementation must store the init-time segment-index-to-device mapping explicitly and guard against the segment at that index changing identity after a vector rebuild.

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:13:21.670Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), `seg.freeze` means "do not run the effect function (_mode[seg.mode]())" — it does NOT mean "skip show()". A frozen segment's pixel buffer can still be updated externally (e.g., realtime control or single-pixel JSON API). `strip.trigger()` is the primary mechanism to flush those externally written pixels to the LEDs on the next service tick. Therefore, frozen segments must remain part of the `doShow`/`show()` path, and it is architecturally wrong to exclude frozen segments from `doShow`. Do not suggest skipping frozen segments from the show path in future reviews.

Learnt from: DedeHai
Repo: wled/WLED PR: 4889
File: wled00/json.cpp:310-310
Timestamp: 2026-03-21T18:12:09.437Z
Learning: In WLED's `deserializeSegment()` (wled00/json.cpp), the blend mode field `seg.blendMode` is intentionally written without a post-read clamp (`getVal(elem["bm"], seg.blendMode)`). Out-of-range or unsupported blend mode values are handled safely in `WS2812FX::blendSegment()` (wled00/FX_fcn.cpp), which defaults to mode 0 for any unsupported value via a bounds check against the `BLENDMODES` constant. Do not flag the missing clamp in deserializeSegment as a bug in future reviews.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-25T12:25:11.610Z
Learning: In WLED's `Segment::startTransition()` (wled00/FX_fcn.cpp:284), when `isInTransition()` is true and `_t->_oldSegment` already exists, the function silently returns without updating `_t->_start` or `_t->_bri`. This causes a bug where rapid successive on/off toggles during a non-FADE blending transition (e.g., fairy dust) leave the transition clock stale: by the time of the second re-trigger, elapsed time may already exceed `_dur`, so `updateTransitionProgress()` sets `_progress = 0xFFFF` immediately on the next service tick and `stopTransition()` fires — the blending effect never plays. The fix is to always reset `_t->_start = millis()`, `_t->_dur = dur`, and `_t->_bri = currentBri()` (current visible brightness) in the `isInTransition()` branch, regardless of whether `_oldSegment` exists.

Learnt from: willmmiles
Repo: wled/WLED PR: 4712
File: wled00/FX_fcn.cpp:992-993
Timestamp: 2025-06-07T15:58:42.579Z
Learning: In WLED segments, the backing pixel buffer is always allocated to the full physical segment size, but effect rendering only uses a subset when mirror or grouping are enabled. For 2D segments, this subset is vWidth() * vHeight(), and for 1D segments it's vLength(). Color processing operations like fade_out should iterate over this rendering subset, not the full allocated buffer.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-10T11:41:46.726Z
Learning: In the WLED repository (wled/WLED), trailing-whitespace removals or other pure-whitespace "optimizations" must NOT be intermixed with real code/logic changes in the same PR. Reviewers prefer whitespace cleanup to be submitted in a dedicated, separate PR. Reason: intermixing makes diffs harder to read (obscures actual logic changes) and causes trouble when cherry-picking commits. Always flag PRs that mix whitespace-only edits with functional changes and ask the contributor to separate them.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/set.cpp:551-555
Timestamp: 2026-03-27T21:00:25.902Z
Learning: In WLED PR `#5048` (wled00/set.cpp lines 551-555), the CONFIG_IDF_TARGET_ESP32C5 block that unconditionally forces ntpEnabled = false is an intentional **temporary hotfix** by softhack007 for a known ESP32-C5 crash: `assert failed: udp_new_ip_type udp.c:1278 (Required to lock TCPIP core functionality!)`. Do not flag this as a permanent design issue; the TODO comment in the code already notes it should be resolved properly once the underlying IDF/TCPIP bug on C5 is fixed. A future permanent solution should use a target capability flag rather than overwriting the user's setting.

Learnt from: softhack007
Repo: wled/WLED PR: 5457
File: usermods/zigbee_rgb_light/usermod_zigbee_rgb_light.h:0-0
Timestamp: 2026-03-31T17:31:01.023Z
Learning: In WLED PR `#5457` (zigbee_rgb_light usermod): The WLED_MAX_DIGITAL_CHANNELS=0 build flag used in the esp32c6_zigbee environment is a temporary workaround for rmt_tx_wait_all_done() timeout spam when the Zigbee/802.15.4 stack is active. The root cause is under investigation and is likely related to Zigbee light-sleep (CONFIG_PM_ENABLE) disrupting RMT's internal time base, or ISR latency due to cache-disable during flash ops — NOT the 802.15.4 radio "sharing" the RMT peripheral (they are separate hardware). Because a proper fix (rmt_enable()/rmt_disable() PM-lock wrapping, allow_pd=0, CONFIG_RMT_TX_ISR_CACHE_SAFE) may eliminate the need to disable digital channels entirely, do NOT add a compile-time `#error` guard requiring WLED_MAX_DIGITAL_CHANNELS=0; doing so would prematurely bake in a constraint that may be lifted once the investigation concludes.

Learnt from: softhack007
Repo: wled/WLED PR: 5480
File: docs/cpp.instructions.md:511-511
Timestamp: 2026-04-08T19:10:48.754Z
Learning: In WLED documentation and review guidance (`docs/cpp.instructions.md`), using `WLED_MAX_SEGNAME_LEN = 64` as the example value in LittleFS filename-length checks is intentional and correct. The reasoning: ESP32 uses 64-char segment names (the larger default), and if a constructed path (e.g. `/ledmap_` + segment name + `.json`) stays within 255 bytes when the name is 64 chars, it is trivially safe on ESP8266 where segment names are limited to 32 chars. WLED core has no ESP8266-only features, so validating against the larger ESP32 default is the right cross-platform conservative choice. Do NOT flag `WLED_MAX_SEGNAME_LEN = 64` in this guidance as misleading or platform-specific.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:30.955Z
Learning: In WLED code reviews, verify that file operations (especially file.open()) respect LittleFS filename limitations. Assume default WLED configuration with LittleFS default filename limit of 255 bytes. Do not assume extreme configuration values like WLED_MAX_SEGNAME_LEN = 512 which would not be standard configurations.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: platformio.ini:149-150
Timestamp: 2026-03-29T16:47:56.452Z
Learning: In WLED PR `#4838` (ESP-IDF V5 branch, platformio.ini): The original FastLED library has been replaced with a custom `fastled-slim` fork. As a result, the `[v5_pioarduino_workaround]` section (containing `lib_archive = yes`) and the commented-out `post:pio-scripts/fastled_cxx_workaround.py` extra script are intentional dead code pending removal after back-to-back testing. Do NOT flag `lib_archive = yes` in `[v5_pioarduino_workaround]` as unreferenced or suggest moving it to active env blocks — the entire section is slated for removal.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

@softhack007 softhack007 added the needs_rework PR needs improvements before merging (RED FLAG) label Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Partly generated by an AI. Make sure that the contributor fully understands the code! enhancement needs_rework PR needs improvements before merging (RED FLAG)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emulate Alexa devices per segment

3 participants