Skip to content

make per-segment on/off and brightness behave the same as global#5524

Draft
DedeHai wants to merge 3 commits intowled:mainfrom
DedeHai:fix_single_segment_transition
Draft

make per-segment on/off and brightness behave the same as global#5524
DedeHai wants to merge 3 commits intowled:mainfrom
DedeHai:fix_single_segment_transition

Conversation

@DedeHai
Copy link
Copy Markdown
Collaborator

@DedeHai DedeHai commented Apr 22, 2026

fix for #5520

Summary by CodeRabbit

  • Bug Fixes
    • More reliable segment transitions when another transition is already active.
    • Smoother, more consistent brightness blending (fixes on/off brightness glitches).
    • Opacity changes now start predictable transitions.
    • Per-segment off pixels no longer show residual colors.
    • Global brightness transitions now start only on actual brightness changes; one‑shot transition delays supported.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Walkthrough

Changes adjust Segment transition handling (startTransition, currentBri, setOpacity), modify WS2812FX::blendSegment non-FADE off-state rendering, and restrict global transition start in stateUpdated() to brightness changes only; also add jsonTransitionOnce one-time transition delay when bri is unchanged.

Changes

Cohort / File(s) Summary
Segment transition logic & brightness
wled00/FX_fcn.cpp
Reworked Segment::startTransition() behavior when a transition is already active: if segmentCopy requested but _t->_oldSegment missing, non-FADE transitions are cancelled; for TRANSITION_FADE an _oldSegment is created/reset, timing and _t->_bri are (re)initialized; when already transitioning and segmentCopy==false, the transition restarts from current visible brightness.
currentBri & setOpacity
wled00/FX_fcn.cpp
Segment::currentBri() now uses std::max(curBri, _t->_bri) for non-FADE on/off differences instead of prior previous-mode check; Segment::setOpacity(o) always starts transitions with segmentCopy=false.
Renderer blending workaround
wled00/FX_fcn.cpp
WS2812FX::blendSegment() non-FADE slow paths now also force per-segment off-state pixels to black (c_a = BLACK) in addition to global on/off handling.
Global transition trigger & JSON one-time delay
wled00/led.cpp
In stateUpdated(byte), starting/restarting global brightness transitions is now driven only by bri != briOld; if already transitioning, briOld is reset from briT before restarting. If bri == briOld and jsonTransitionOnce is set, strip.setTransition(transitionDelay) is applied once and cleared.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant LEDController
    participant SegmentManager
    participant Renderer

    Client->>LEDController: update state (bri, on/off, jsonTransitionOnce)
    LEDController->>LEDController: if bri != briOld -> set/restart global transition timer
    LEDController->>SegmentManager: startTransition(segmentCopy? / blendingStyle)
    alt segmentCopy requested but no _oldSegment & non-FADE
        SegmentManager->>SegmentManager: stopTransition() (cancel)
    else TRANSITION_FADE or _oldSegment exists
        SegmentManager->>SegmentManager: create/reset _oldSegment, set _t->_start/_dur/_bri
    end
    SegmentManager->>Renderer: provide segment state for blending
    Renderer->>Renderer: blendSegment(): apply opacity/brightness
    Renderer->>Renderer: if per-segment off (non-FADE) -> force BLACK pixel
    Renderer-->>Client: output pixels
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • softhack007
  • willmmiles
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'make per-segment on/off and brightness behave the same as global' directly and clearly summarizes the main objective of the changeset across both modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/FX_fcn.cpp`:
- Around line 289-313: When isInTransition() returns early the active transition
(_t) isn't refreshed, causing stale progress; update the transition timing and
brightness before any early return: inside the isInTransition() branch of
Segment::startTransition() (the branch that currently returns without creating a
copy or when segmentCopy is false), set _t->_start = millis(), _t->_dur = dur,
and _t->_bri = currentBri() (and optionally reset _t->_prevPaletteBlends) before
returning so rapid per-segment retriggers refresh the active transition state
rather than jump or immediately finish.

In `@wled00/led.cpp`:
- Around line 131-146: When a transition is active do not overwrite the
previous-completed baseline (briOld) with the in-flight interim value (briT);
instead introduce and use a separate requested/target brightness variable (e.g.,
briTarget) to detect intentional brightness changes and only restart global
transitions when bri != briTarget (or when briTarget is updated), keep
transitionActive/transitionStartTime logic tied to that explicit target change,
and remove the commented-out old branch; update references to briOld, briT, bri,
transitionActive, strip.setTransitionMode, transitionStartTime,
jsonTransitionOnce and transitionDelay accordingly so segment-only updates
during an active transition do not reset the global transition timer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 685a4033-b915-4810-8901-d106ef11acb0

📥 Commits

Reviewing files that changed from the base of the PR and between d20dbeb and 5a8d5e4.

📒 Files selected for processing (2)
  • wled00/FX_fcn.cpp
  • wled00/led.cpp

Comment thread wled00/FX_fcn.cpp
Comment thread wled00/led.cpp Outdated
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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
wled00/FX_fcn.cpp (1)

289-325: ⚠️ Potential issue | 🟠 Major

Preserve the in-flight brightness when replacing a no-copy transition.

When Line 295 stops an active non-FADE brightness transition, the replacement transition at Line 325 is initialized from raw on ? opacity : 0 instead of the visibleBri captured at Line 290. Mid-transition on/off or effect retargets can still jump to the target brightness instead of continuing from the visible level.

Proposed fix
 void Segment::startTransition(uint16_t dur, bool segmentCopy) {
   if (dur == 0 || !isActive()) {
     if (isInTransition()) _t->_dur = 0;
     return;
   }
+  uint8_t initialBri = on ? opacity : 0;
   if (isInTransition()) {
     const uint8_t visibleBri = currentBri();
     if (segmentCopy && !_t->_oldSegment) {
       if (blendingStyle != TRANSITION_FADE) {
         // transition without a copy means we are in a segment brightness transition
         // need to cancel the transition to not interfere with the new one (or global brightness change)
         stopTransition();
+        initialBri = visibleBri;
       } else {
         // already in FADE transition but segment copy requested  and not yet created (for example a SWIPE color change during brightness transition)
         // create segment copy and restart timer so currentBri() can interpolate correctly
         _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings
@@
   _t = new(std::nothrow) Transition(dur);
   if (_t) {
-    _t->_bri = on ? opacity : 0;
+    _t->_bri = initialBri;

Based on learnings: Segment::startTransition() should reset _t->_start = millis(), _t->_dur = dur, and _t->_bri = currentBri() in the isInTransition() branch.

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

In `@wled00/FX_fcn.cpp` around lines 289 - 325, The new Transition allocated in
Segment::startTransition() is initialized from raw on ? opacity : 0 after
stopping a non-FADE in-flight brightness transition, which loses the currently
visible brightness; when replacing a running transition (isInTransition() branch
that calls stopTransition()) set the new transition's timing and base brightness
to continue from the visible level by assigning _t->_start = millis(), _t->_dur
= dur and _t->_bri = currentBri() (or visibleBri) after _t = new(std::nothrow)
Transition(dur) so the new _t uses the interpolated brightness rather than on ?
opacity : 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/FX_fcn.cpp`:
- Around line 373-377: The non-FADE branch always applies max(curBri, _t->_bri)
when _t->_oldSegment exists, causing brightness to stick when only brightness
changed; modify the logic so the max rule is used only when the old and new "on"
states differ (compare _t->_oldSegment->on vs _t->on or equivalent), otherwise
perform the same interpolation used for FADE (use the prog-based blend formula:
prog * curBri + _t->_bri * (0xFFFFU - prog) divided by 0xFFFFU). Update the
conditional around blendingStyle/ _t->_oldSegment to check the on-state
difference and apply max only in that case.

---

Duplicate comments:
In `@wled00/FX_fcn.cpp`:
- Around line 289-325: The new Transition allocated in
Segment::startTransition() is initialized from raw on ? opacity : 0 after
stopping a non-FADE in-flight brightness transition, which loses the currently
visible brightness; when replacing a running transition (isInTransition() branch
that calls stopTransition()) set the new transition's timing and base brightness
to continue from the visible level by assigning _t->_start = millis(), _t->_dur
= dur and _t->_bri = currentBri() (or visibleBri) after _t = new(std::nothrow)
Transition(dur) so the new _t uses the interpolated brightness rather than on ?
opacity : 0.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47bc4210-0d1e-4aee-a40a-9afd0be79ae9

📥 Commits

Reviewing files that changed from the base of the PR and between 5a8d5e4 and 65c2f83.

📒 Files selected for processing (2)
  • wled00/FX_fcn.cpp
  • wled00/led.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • wled00/led.cpp

Comment thread wled00/FX_fcn.cpp Outdated
@DedeHai
Copy link
Copy Markdown
Collaborator Author

DedeHai commented Apr 22, 2026

@coderabbitai please compile a test plan to verify the changes, focus on edge cases that might behave incorrectly. also check if any externally applied changes by sync or home assistent etc. work

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Tip

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

🧩 Analysis chain

Let me pull the relevant code sections to write a precise test plan.


🏁 Script executed:

#!/bin/bash
# Read the transition-related functions in FX_fcn.cpp
rg -n "startTransition|currentBri|setOpacity|blendSegment" wled00/FX_fcn.cpp | head -60

# Read stateUpdated in led.cpp
rg -n "stateUpdated|briOld|briT|jsonTransitionOnce|transitionDelay" wled00/led.cpp | head -60

Length of output: 2819


🏁 Script executed:

#!/bin/bash
# Read startTransition function
sed -n '284,370p' wled00/FX_fcn.cpp

# Read currentBri function
sed -n '369,420p' wled00/FX_fcn.cpp

# Read setOpacity function
sed -n '549,570p' wled00/FX_fcn.cpp

# Read blendSegment non-FADE on/off handling
sed -n '1386,1530p' wled00/FX_fcn.cpp

Length of output: 15360


🏁 Script executed:

#!/bin/bash
# Read blendSegment slow path on/off section (after the clipping setup)
sed -n '1530,1730p' wled00/FX_fcn.cpp

# Read full stateUpdated and surrounding context in led.cpp
sed -n '85,200p' wled00/led.cpp

Length of output: 15194


@DedeHai Here is a comprehensive test plan for PR #5524, focusing on edge cases and external-integration paths.


Test Plan — PR #5524: Per-segment on/off & brightness transitions

A. Per-segment on/off with non-FADE blending style (e.g. Fairy Dust, Swipe)

# Scenario Expected Changed code path
A1 Turn one segment OFF from idle (no prior transition) — non-FADE style Non-FADE transition plays; off-region pixels forced to BLACK via if (segO && !seg->on) c_a = BLACK blendSegment() slow-path
A2 Turn one segment ON from idle — non-FADE style Transition plays; std::max(curBri, _t->_bri) keeps brightness correct from the first frame currentBri()
A3 Rapid OFF → ON while non-FADE transition is still running (no _oldSegment yet) stopTransition() is now called; new transition restarts cleanly from current visible brightness — no stuck/stale clock startTransition()
A4 Rapid ON → OFF while non-FADE transition is still running Same as A3 — no ghost brightness startTransition()
A5 Repeat A3/A4 five times quickly (stress) Each cycle produces a full, clean transition; no permanent dim or full-bright stuck state startTransition(), currentBri()

B. setOpacity() — segmentCopy flag change

# Scenario Expected Changed code path
B1 Fade opacity slider from 255 → 128 with non-FADE blending style segmentCopy=false is passed — no segment copy allocated; brightness crossfades using _t->_bri interpolation only setOpacity()
B2 Rapid opacity changes mid-transition (non-FADE) Transition clock restarted each time from currentBri(); no allocation loop / memory exhaustion startTransition() restart branch
B3 Opacity change while FADE blending style Behavior unchanged — FADE branch not affected setOpacity()

C. FADE style — new copy-creation branch

# Scenario Expected Changed code path
C1 Trigger a SWIPE/color change while a per-segment FADE brightness transition is in progress (no _oldSegment yet) _oldSegment is now created, timer reset, _bri set to visible brightness; crossfade is smooth with no color pop startTransition() FADE+copy branch
C2 Same as C1 but the newly allocated _oldSegment has isActive() == false stopTransition() is called immediately; no orphaned _oldSegment pointer startTransition() FADE+copy branch

D. currentBri() — std::max for non-FADE on/off

# Scenario Expected Changed code path
D1 Turn off segment (non-FADE, _oldSegment exists): progress goes 0→0xFFFF Brightness is std::max(curBri=0, _t->_bri=old_opacity) = remains at old_opacity until effect clips it to BLACK currentBri()
D2 Turn on segment (non-FADE, _oldSegment exists): progress goes 0→0xFFFF std::max(curBri=opacity, _t->_bri=0) = opacity from frame 1; no dark flicker at transition start currentBri()
D3 Non-FADE style but not an on/off transition (e.g. palette-only change, _oldSegment->on == on) Falls back to FADE-blend path ((prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU) currentBri()

E. Global brightness re-targeting (led.cpp)

# Scenario Expected Changed code path
E1 Brightness slider 0 → 255 during an ongoing 0 → 128 transition briOld = briT captures mid-flight value; interpolation restarts from there — no brightness jump stateUpdated()
E2 Three rapid brightness changes before any transition completes Each re-target uses the current briT; final value is reached smoothly stateUpdated()
E3 Brightness change while bri == briOld but jsonTransitionOnce = true strip.setTransition(transitionDelay) is applied and flag cleared; no transition fires on brightness (it's equal), but the next effect change uses the temporary transition duration stateUpdated()

F. jsonTransitionOnce with equal brightness

# Scenario Expected Changed code path
F1 JSON payload sets same brightness + tt field (one-time transition) setTransition(transitionDelay) called, jsonTransitionOnce cleared; subsequent color/effect change uses the specified duration stateUpdated()
F2 Preset recall with matching brightness Same as F1 — transition delay applied even though bri didn't change stateUpdated()

G. Multiple segments interaction

# Scenario Expected Changed code path
G1 Two non-overlapping segments: one OFF (non-FADE transition), one ON if (segO && !seg->on) c_a = BLACK only sets the off-segment's pixels to BLACK; on-segment unaffected blendSegment() slow-path
G2 Two overlapping segments: both in simultaneous non-FADE on/off transitions Each segment's pixels independently forced BLACK in their off-state; no cross-segment bleed blendSegment() slow-path
G3 Single-pixel segment with non-FADE style blendingStyle is forced to TRANSITION_FADE for width*height==1; std::max branch is never reached blendSegment() / currentBri()

H. External integrations — Home Assistant, Sync, MQTT, Alexa, Presets

# Scenario Expected Notes
H1 Home Assistant sends {"on":false,"transition":1} to a single segment via JSON API Per-segment off-transition behaves identically to UI toggle; blendSegment() BLACK-forcing applies Full HTTP JSON path
H2 Home Assistant sends brightness change mid-transition ({"bri":100}) briOld = briT re-targeting applies (same as UI slider); no jump stateUpdated()
H3 UDP Sync (CALL_MODE_NOTIFICATION) delivers a brightness change while local transition is active stateUpdated() is called; transition re-targeting (briOld = briT) still runs because the bri != briOld branch is not gated on callMode stateUpdated()
H4 UDP Sync delivers a per-segment on/off change mid-non-FADE transition startTransition() restart branch fires; stopTransition() called if segmentCopy=true + non-FADE + no _oldSegment startTransition()
H5 MQTT cmnd/wled/api sends {"seg":[{"on":false}]} Same path as HA JSON; per-segment transition triggers correctly Full JSON path
H6 Alexa brightness change (0–100% mapped to 0–255) Hits setOpacity() → now always segmentCopy=false regardless of blend style setOpacity()
H7 Preset load that flips multiple segments on/off simultaneously Each segment calls startTransition() independently; non-FADE segments get stopTransition() + fresh start, FADE segments get copy startTransition()
H8 Playlist cycling with jsonTransitionOnce mid-cycle while brightness is stable setTransition(transitionDelay) is applied for the effect change; no spurious brightness interpolation stateUpdated()

I. Global on/off vs per-segment on/off interaction in blendSegment()

# Scenario Expected Notes
I1 Global bri = 0 (off) while a per-segment non-FADE transition is active Both the global (briOld==0 || bri==0) condition and the new if (segO && !seg->on) condition may apply; pixels go BLACK, no conflict Dual condition in blendSegment()
I2 Global on (briOld=0 → bri>0) while a segment is individually off Global path sets clipped pixels to BLACK; per-segment !seg->on also sets BLACK — double BLACK is harmless blendSegment() slow-path
I3 Segment is individually on but global bri is transitioning 0→255 seg->on == true, so !seg->on path is skipped; global fade applies normally blendSegment() slow-path

J. Memory safety

# Scenario Expected Notes
J1 stopTransition() called inside startTransition() (non-FADE, segmentCopy, no _oldSegment) followed immediately by the normal fresh allocation path _t is deleted, then new Transition(dur) allocates fresh — no double-free, no use-after-free startTransition()
J2 new(std::nothrow) Segment(*) for _oldSegment returns nullptr (OOM) in FADE copy branch Guard if (_t->_oldSegment) prevents null dereference; stopTransition() is called if allocation fails startTransition() FADE copy branch

Recommended test sequence (manual, on hardware)

  1. Set a non-FADE blending style (e.g. Fairy Dust or Swipe Right), transition duration ~800 ms.
  2. Run scenarios A3/A4/A5 — rapidly toggle segment on/off via UI, HA, and MQTT within the transition window.
  3. Set transition to ~1000 ms and drag the brightness slider repeatedly mid-transition (scenario E1/E2).
  4. Use a Home Assistant automation to fire on → off → on within 200 ms intervals (covers H1–H4).
  5. Confirm briOld = briT re-targeting by logging briT over serial during rapid HA brightness commands.
  6. Load a preset with 3+ segments in different on/off states and verify each transitions independently (H7).
  7. Test with a single-pixel segment to confirm FADE-override path (G3).

🧠 Learnings used
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: 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: 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: 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: 5508
File: wled00/FX.cpp:4137-4142
Timestamp: 2026-04-16T09:50:07.774Z
Learning: WLED overlay behavior: Effects must operate via SEGMENT.* APIs (reads/writes) — global frame reads (strip.getPixelColor*) are reserved for copy_* style post‑processing. Blending between segments is performed later in WS2812FX::blendSegment(); effects should not try to composite with lower layers themselves.

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-10T11:41:46.737Z
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: 4923
File: wled00/FX.cpp:4883-4901
Timestamp: 2025-09-12T17:29:43.826Z
Learning: In WLED’s web UI, only one slider value (e.g., SEGMENT.intensity or SEGMENT.custom1) changes at a time; code relying on this may use simplified change guards, though presets/JSON can still update multiple fields atomically.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-04-10T16:10:42.477Z
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: 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.

@coderabbitai

This comment was marked as duplicate.

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.

🧹 Nitpick comments (1)
wled00/FX_fcn.cpp (1)

284-340: startTransition() rework addresses prior feedback — LGTM with one small nit.

The isInTransition() branch now consistently refreshes _t->_start, _t->_dur, _t->_bri, and _t->_prevPaletteBlends (both in the FADE+no-copy-yet path and in the generic “already transitioning” path), and the non-FADE+no-copy-yet case cleanly cancels and falls through to reallocate. This resolves the stale-clock / immediate-stopTransition issue that was flagged on the previous revisions.

One minor observation: in the non-FADE+no-copy-yet branch (lines 292–295), visibleBri is computed but discarded — after stopTransition() the fall-through path at lines 322–325 sets _t->_bri = on ? opacity : 0. Combined with the new setOpacity(..., false) behaviour, this means a mode/color change that interrupts an in-flight per-segment opacity fade in a non-FADE style will seed the fresh transition from the target opacity rather than the currently visible brightness, so the user may see a small brightness snap at the moment of interruption. Not a correctness bug; noting in case it was not intentional.

Optional tweak to preserve visible brightness across the cancel-and-restart
     if (segmentCopy && !_t->_oldSegment) {
       if (blendingStyle != TRANSITION_FADE) {
         // transition without a copy means we are in a segment brightness transition
         // need to cancel the transition to not interfere with the new one (or global brightness change)
         stopTransition();
+        // fall through; seed the new transition from the currently visible brightness
+        _t = new(std::nothrow) Transition(dur);
+        if (_t) {
+          _t->_bri = visibleBri;
+          _t->_cct = cct;
+          _t->_palette = palette;
+          `#ifndef` WLED_SAVE_RAM
+          loadPalette(_t->_palT, palette);
+          `#endif`
+          for (int i=0; i<NUM_COLORS; i++) _t->_colors[i] = colors[i];
+          if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this);
+          if (_t->_oldSegment && !_t->_oldSegment->isActive()) stopTransition();
+        }
+        return;
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX_fcn.cpp` around lines 284 - 340, The code in
Segment::startTransition computes visibleBri but discards it when the
non-FADE+no-copy-yet path calls stopTransition(), causing the new Transition to
be seeded from on? opacity : 0 and producing a visible snap; preserve the
computed visibleBri and use it to initialize the new transition's brightness.
Concretely: when taking the branch that calls stopTransition(), save the
visibleBri value and after allocating _t = new Transition(dur) (or before
assigning _t->_bri) set _t->_bri = visibleBri instead of leaving the existing on
? opacity : 0 behavior (ensure this applies both to the allocate-path and any
restart-paths that follow stopTransition()). Reference symbols:
Segment::startTransition, visibleBri, stopTransition(), _t = new(std::nothrow)
Transition(dur), and _t->_bri.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@wled00/FX_fcn.cpp`:
- Around line 284-340: The code in Segment::startTransition computes visibleBri
but discards it when the non-FADE+no-copy-yet path calls stopTransition(),
causing the new Transition to be seeded from on? opacity : 0 and producing a
visible snap; preserve the computed visibleBri and use it to initialize the new
transition's brightness. Concretely: when taking the branch that calls
stopTransition(), save the visibleBri value and after allocating _t = new
Transition(dur) (or before assigning _t->_bri) set _t->_bri = visibleBri instead
of leaving the existing on ? opacity : 0 behavior (ensure this applies both to
the allocate-path and any restart-paths that follow stopTransition()). Reference
symbols: Segment::startTransition, visibleBri, stopTransition(), _t =
new(std::nothrow) Transition(dur), and _t->_bri.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd30466f-9e4c-4e12-8cb4-af32575631cb

📥 Commits

Reviewing files that changed from the base of the PR and between 65c2f83 and cd33b13.

📒 Files selected for processing (1)
  • wled00/FX_fcn.cpp

@DedeHai
Copy link
Copy Markdown
Collaborator Author

DedeHai commented Apr 22, 2026

performed all tests except the ones involving HA and MQTT and all behave correctly

@blazoncek
Copy link
Copy Markdown
Contributor

If you want my opinion: This incorrectly solves OP issue. It also changes behaviour of non-segment-copy transitions.
The real OP culprit lies elsewhere.

@DedeHai
Copy link
Copy Markdown
Collaborator Author

DedeHai commented Apr 23, 2026

yea, I am not too convinced this is the proper let alone best solution. If you find something better, please keep me posted.

@DedeHai DedeHai marked this pull request as draft April 23, 2026 12:10
@blazoncek
Copy link
Copy Markdown
Contributor

I think I've solved the issue (all of them). Watch my fork for changes after I test them.

@softhack007 softhack007 added this to the 16.1 milestone Apr 28, 2026
@softhack007 softhack007 linked an issue Apr 28, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New transitions also for individual segments.

3 participants