esp32-p4 support - #54
Conversation
WalkthroughAdds ESP32‑P4 PARLIO TX support and integrates it into a hardware‑neutral driver lifecycle (hwInit/hwStart/hwStop); replaces per‑component indices with channels‑per‑light plus ColorArrangement helpers; reorganizes buffer lifecycle (init/delete), refactors update/deleteDriver flows, adjusts macros, PlatformIO deps, and documentation. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application
participant Driver as I2SClocklessLedDriver
participant Buffers as PSRAM/Buffers
participant PARLIO as PARLIO_TX_Unit
App->>Driver: initled(...) / showPixels(WAIT)
alt first P4 transmit (lazy init)
Driver->>Driver: hwInit() / ensureParlioTxUnitInitialized()
Driver->>Buffers: initBuffers() (allocate ping/pong)
Driver->>Driver: loadAndTranspose() -> build waveform chunks
Driver->>PARLIO: parlio_tx_unit_transmit(chunk) [non-blocking]
PARLIO-->>Driver: async transmit complete
else subsequent transmits
Driver->>Driver: loadAndTranspose() (swap ping/pong)
Driver->>PARLIO: parlio_tx_unit_transmit(chunk)
end
Driver-->>App: showPixels returns / completion signaled
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
docs/developer/developer.md (1)
83-83: Optional: Minor style improvement.Consider simplifying "pin numbers" to "pins" for conciseness.
📝 Suggested change
-On ESP32-P4, `initLedImpl()` stores the pin numbers in `p4Pins[]` and calls `setBrightness()` to initialise the LUT tables, then returns immediately — no I2S peripheral or DMA buffer allocation takes place. +On ESP32-P4, `initLedImpl()` stores the pins in `p4Pins[]` and calls `setBrightness()` to initialise the LUT tables, then returns immediately — no I2S peripheral or DMA buffer allocation takes place.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/developer.md` at line 83, The sentence uses "pin numbers" which can be shortened to "pins" for conciseness; update the documentation sentence referencing initLedImpl(), p4Pins[], setBrightness(), showPixels(), and PARLIO to replace "pin numbers" with "pins" (e.g., "stores the pins in p4Pins[]") while keeping the rest of the description identical.src/parlio_p4.cpp (1)
336-336: ReconsiderIRAM_ATTRon this function.
show_parlio_p4is a large function (~160 lines) with extensive computation (bit transposition, buffer packing). Placing it entirely in IRAM consumes significant instruction RAM. Since this function is called from task context (not ISR),IRAM_ATTRis not required and may waste scarce IRAM space.♻️ Suggested change
-uint8_t IRAM_ATTR __attribute__((hot)) show_parlio_p4( +uint8_t __attribute__((hot)) show_parlio_p4(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` at line 336, The function show_parlio_p4 is annotated with IRAM_ATTR which forces the entire ~160-line routine into limited IRAM; remove the IRAM_ATTR (i.e., strip IRAM_ATTR from the show_parlio_p4 declaration) so it lives in normal flash since it runs in task context, and only keep IRAM_ATTR on small, true ISR-critical helpers if any; verify no callers expect it to be IRAM-resident and rebuild to confirm reduced IRAM usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 122-135: The build fails because PARLIO symbols used in
I2SClocklessLedDriver.cpp (parlio_tx_unit_wait_all_done, parlio_tx_unit_disable,
parlio_del_tx_unit and related types/variables like p4TxUnit,
p4Buffer1/p4Buffer2) are not declared; add `#include` "driver/parlio_tx.h" to
I2SClocklessLedDriver.h inside the CONFIG_IDF_TARGET_ESP32P4 guard so the PARLIO
types and functions are visible to the compilation unit that uses them.
In `@src/I2SClocklessLedDriver.h`:
- Around line 354-380: initLedImpl() currently calls heap_caps_calloc_prefer to
allocate p4Buffer1 and p4Buffer2 but does not check for allocation failure;
update initLedImpl() to validate p4Buffer1 and p4Buffer2 after allocation, and
if either is NULL free any partially allocated buffer, log an error, set
relevant state (e.g., p4SetupDone = false) and return a failure status so
callers (showPixels()/updateDriver()) never pass nullptr into
create_transposed_led_output_optimized; also ensure deleteDriver() can safely
handle partially-initialized buffers by guarding frees and nulling
p4Buffer1/p4Buffer2/p4BufferActive.
- Around line 26-43: The build fails because the ESP32-P4 block in
I2SClocklessLedDriver.h uses PARLIO types parlio_tx_unit_handle_t and
parlio_tx_unit_config_t but never includes the PARLIO header; fix by adding the
appropriate PARLIO header (e.g., add `#include` "driver/parlio_tx.h") inside the
CONFIG_IDF_TARGET_ESP32P4 branch so parlio_tx_unit_handle_t and
parlio_tx_unit_config_t used by the I2SClocklessLedDriver class are defined.
In `@src/parlio_p4.cpp`:
- Around line 185-195: Ensure we defensively check driver LUT pointers before
dereferencing in the packet assembly: in the block that writes packetRGBChannel
using offsetWhite, offsetWhite2 and red/green/blue maps, verify
driver->whiteMap, driver->white2Map, driver->redMap, driver->greenMap and
driver->blueMap are non-null (or fall back to identity mapping) before indexing
them; update the logic around packetRGBChannel[offsetWhite],
packetRGBChannel[offsetWhite2], packetRGBChannel[offsetRed],
packetRGBChannel[offsetGreen], and packetRGBChannel[offsetBlue] (or call a
helper that returns a safe mapped value) so show_parlio_p4 cannot crash if
setBrightness() hasn't initialized the LUTs.
- Around line 481-484: The timing calls use Arduino-only functions; replace
micros() with esp_timer_get_time() and delayMicroseconds(20) with
esp_rom_delay_us(20) and add the necessary ESP-IDF headers (include
"esp_timer.h" and "esp_rom_sys.h"); update the local timestamp variables used
around parlio_tx_unit_wait_all_done (e.g., before/after) from unsigned long to a
64-bit type (int64_t) to match esp_timer_get_time() return type so the
comparison and conditional delay remain correct.
---
Nitpick comments:
In `@docs/developer/developer.md`:
- Line 83: The sentence uses "pin numbers" which can be shortened to "pins" for
conciseness; update the documentation sentence referencing initLedImpl(),
p4Pins[], setBrightness(), showPixels(), and PARLIO to replace "pin numbers"
with "pins" (e.g., "stores the pins in p4Pins[]") while keeping the rest of the
description identical.
In `@src/parlio_p4.cpp`:
- Line 336: The function show_parlio_p4 is annotated with IRAM_ATTR which forces
the entire ~160-line routine into limited IRAM; remove the IRAM_ATTR (i.e.,
strip IRAM_ATTR from the show_parlio_p4 declaration) so it lives in normal flash
since it runs in task context, and only keep IRAM_ATTR on small, true
ISR-critical helpers if any; verify no callers expect it to be IRAM-resident and
rebuild to confirm reduced IRAM usage.
🪄 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: 5b8becdf-30fb-4b36-9076-aba7f6886855
📒 Files selected for processing (8)
CLAUDE.mddocs/developer/developer.mddocs/enduser/enduser.mddocs/index.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/parlio_p4.cppsrc/parlio_p4.h
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/helper.h (1)
13-23:⚠️ Potential issue | 🔴 CriticalRestore the missing
{inRUN_SKETCH_FOR.Line 14 expands to
doinstead ofdo {, but Line 23 closes a block with} while (0). This creates invalid syntax that will break compilation in any translation unit that includes this header.Proposed fix
`#define` RUN_SKETCH_FOR(name, duration, func) \ - do \ + do { \ printf("Start Sketch: %s\n", name); \ uint32_t _timer1_ = ESP.getCycleCount(); \ uint32_t _timer2_ = ESP.getCycleCount(); \ while ((_timer2_ - _timer1_) / 240000 < duration) { \ func; \ _timer2_ = ESP.getCycleCount(); \ } \ printf("End Sketch: %s after %.2fms\n", name, (float)(_timer2_ - _timer1_) / 240000); \ - } while (0) + } while (0)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/helper.h` around lines 13 - 23, The RUN_SKETCH_FOR macro is missing the opening brace after the do, causing invalid syntax; update the macro (identifier RUN_SKETCH_FOR) so the `do` is followed by `{` to enclose the block that uses _timer1_, _timer2_, the while loop and the printf calls, ensuring the closing `} while (0)` matches the opening brace and the macro expands into a valid compound statement.
🤖 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 `@src/helper.h`:
- Around line 13-23: The RUN_SKETCH_FOR macro is missing the opening brace after
the do, causing invalid syntax; update the macro (identifier RUN_SKETCH_FOR) so
the `do` is followed by `{` to enclose the block that uses _timer1_, _timer2_,
the while loop and the printf calls, ensuring the closing `} while (0)` matches
the opening brace and the macro expands into a valid compound statement.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b540dc63-c40d-4cb0-af64-05825de09c71
📒 Files selected for processing (6)
docs/developer/developer.mdplatformio.inisrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/helper.hsrc/parlio_p4.cpp
✅ Files skipped from review due to trivial changes (1)
- src/parlio_p4.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/developer/developer.md
- src/I2SClocklessLedDriver.h
Docs ==== - developer.md: add Repo reorg backend ======= - helper.h: fix do while (0) - CLD: add initErrorOccurred (use in alloc(), initSuccess, add virtual driver vars
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/I2SClocklessLedDriver.h (1)
1529-1569: Duplicated buffer size constant between files.
P4_BUF_BYTEShere (line 1534) andREPACKED_BUFFER_BYTESinparlio_p4.cpp(line 340) both compute1024 × 5 × 32 × 16 / 8 = 327,680. If one is updated without the other, the capacity check inshow_parlio_p4()will silently skip frames when the allocation is actually sufficient (or vice versa).Consider defining a single shared constant in
parlio_p4.hthat both files use.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 1529 - 1569, The code duplicates the buffer-size calculation (P4_BUF_BYTES vs REPACKED_BUFFER_BYTES), so create a single shared constant (e.g., PARLIO_P4_BUFFER_BYTES) in parlio_p4.h, replace the local P4_BUF_BYTES in I2SClocklessLedDriver (the allocation calls referencing p4Buffer1/p4Buffer2 and heap_caps_calloc_prefer) and replace REPACKED_BUFFER_BYTES in parlio_p4.cpp (the capacity check in show_parlio_p4()) to use that shared constant; ensure the header is included where needed and that the constant type/units match the existing allocation and capacity-check logic.src/parlio_p4.cpp (2)
405-428: First frame after topology change is silently dropped.When
outputsormax_ledschanges, the PARLIO unit is reconfigured and the function returns early (line 427), skipping the current frame's transmission. The comment says "give the hardware one frame to settle" but callers may not expect this.Consider either:
- Documenting this in the public API (
show_parlio_p4header comment orshowPixels()docs)- Logging at INFO level so users see it during development:
ESP_LOGI(TAG, "PARLIO reconfigured — skipping warm-up frame");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` around lines 405 - 428, The code silently drops the first frame after PARLIO reconfiguration (the early return after reinitializing driver->p4TxUnit), so either document this behavior in the public API docs (e.g., update show_parlio_p4 header comment or showPixels() docs to state the warm-up frame is skipped) or add a visible log line before the early return to inform users; e.g., emit an info-level message (using ESP_LOGI(TAG, "...PARLIO reconfigured — skipping warm-up frame")) immediately before the "return 0;" that follows the parlio_new_tx_unit/parlio_tx_unit_enable sequence so callers see the skip when topology changes.
24-32: Arduino.h dependency limits portability to pure ESP-IDF builds.The
micros()anddelayMicroseconds()calls (lines 490-493) require Arduino framework. For projects using pure ESP-IDF (no Arduino layer), this file won't compile.If broader compatibility is desired, the ESP-IDF equivalents are straightforward:
`#include` "esp_timer.h" // ... int64_t before = esp_timer_get_time(); // returns microseconds // ... esp_rom_delay_us(20); // already available via esp_rom_sys.hThis is low priority if the library always targets Arduino framework builds.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` around lines 24 - 32, The file currently includes "Arduino.h" and calls micros() and delayMicroseconds(), which prevents pure ESP-IDF builds from compiling; remove the Arduino include and replace the timing calls: include esp_timer.h and esp_rom_sys.h, replace micros() usages with esp_timer_get_time() (int64_t) and replace delayMicroseconds() with esp_rom_delay_us(), updating any timing variables (e.g., before/after) to int64_t as needed; update the include block (remove "Arduino.h", add "esp_timer.h" and "esp_rom_sys.h") and change the calls inside the function where micros() and delayMicroseconds() are used to the ESP-IDF equivalents.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 1529-1569: The code duplicates the buffer-size calculation
(P4_BUF_BYTES vs REPACKED_BUFFER_BYTES), so create a single shared constant
(e.g., PARLIO_P4_BUFFER_BYTES) in parlio_p4.h, replace the local P4_BUF_BYTES in
I2SClocklessLedDriver (the allocation calls referencing p4Buffer1/p4Buffer2 and
heap_caps_calloc_prefer) and replace REPACKED_BUFFER_BYTES in parlio_p4.cpp (the
capacity check in show_parlio_p4()) to use that shared constant; ensure the
header is included where needed and that the constant type/units match the
existing allocation and capacity-check logic.
In `@src/parlio_p4.cpp`:
- Around line 405-428: The code silently drops the first frame after PARLIO
reconfiguration (the early return after reinitializing driver->p4TxUnit), so
either document this behavior in the public API docs (e.g., update
show_parlio_p4 header comment or showPixels() docs to state the warm-up frame is
skipped) or add a visible log line before the early return to inform users;
e.g., emit an info-level message (using ESP_LOGI(TAG, "...PARLIO reconfigured —
skipping warm-up frame")) immediately before the "return 0;" that follows the
parlio_new_tx_unit/parlio_tx_unit_enable sequence so callers see the skip when
topology changes.
- Around line 24-32: The file currently includes "Arduino.h" and calls micros()
and delayMicroseconds(), which prevents pure ESP-IDF builds from compiling;
remove the Arduino include and replace the timing calls: include esp_timer.h and
esp_rom_sys.h, replace micros() usages with esp_timer_get_time() (int64_t) and
replace delayMicroseconds() with esp_rom_delay_us(), updating any timing
variables (e.g., before/after) to int64_t as needed; update the include block
(remove "Arduino.h", add "esp_timer.h" and "esp_rom_sys.h") and change the calls
inside the function where micros() and delayMicroseconds() are used to the
ESP-IDF equivalents.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e020cdea-df7f-47d9-b10b-f76959277874
📒 Files selected for processing (4)
docs/developer/developer.mdsrc/I2SClocklessLedDriver.hsrc/helper.hsrc/parlio_p4.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
- src/helper.h
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/I2SClocklessLedDriver.h (1)
486-563:⚠️ Potential issue | 🟠 Major
setBrightness()setsinitErrorOccurredbut doesn't clearinitSuccess— stale success state persists.When
setBrightness()fails to allocate a LUT (e.g.,redMap), it setsinitErrorOccurred = trueand returns early. However, ifinitSuccesswas already set totruefrom a prior successfulinitLedImpl(), it remainstrue.This creates a window where
showPixelsImpl()proceeds (because it only checksinitSuccess, notinitErrorOccurred) but LUT pointers may be null or partially allocated, leading to the crash risk inrgbwBufferMapping.🐛 Proposed fix
void setBrightness(uint8_t brightness) { this->brightness = brightness; // Allocate LUTs if not already allocated if (!redMap) { redMap = (uint8_t*)malloc(256); if (!redMap) { ESP_LOGE(TAG, "Failed to allocate redMap!"); initErrorOccurred = true; + initSuccess = false; // Prevent showPixels() from proceeding return; } } if (!greenMap) { greenMap = (uint8_t*)malloc(256); if (!greenMap) { ESP_LOGE(TAG, "Failed to allocate greenMap!"); initErrorOccurred = true; + initSuccess = false; return; } } if (!blueMap) { blueMap = (uint8_t*)malloc(256); if (!blueMap) { ESP_LOGE(TAG, "Failed to allocate blueMap!"); initErrorOccurred = true; + initSuccess = false; return; } } if (pW != UINT8_MAX) { if (!whiteMap) { whiteMap = (uint8_t*)malloc(256); if (!whiteMap) { ESP_LOGE(TAG, "Failed to allocate whiteMap!"); initErrorOccurred = true; + initSuccess = false; return; } } } else { free(whiteMap); whiteMap = nullptr; } if (pW2 != UINT8_MAX) { if (!white2Map) { white2Map = (uint8_t*)malloc(256); if (!white2Map) { ESP_LOGE(TAG, "Failed to allocate white2Map!"); initErrorOccurred = true; + initSuccess = false; return; } } } else { free(white2Map); white2Map = nullptr; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 486 - 563, The setBrightness() function can set initErrorOccurred on allocation failure but leaves initSuccess true from a prior init, allowing showPixelsImpl() to run with null LUTs; update setBrightness() to clear initSuccess whenever an allocation fails (set initSuccess = false at each error return) and also proactively clear initSuccess at the start of setBrightness() (or set initErrorOccurred = false and initSuccess = false before allocations) so rgbwBufferMapping and showPixelsImpl() never run with partially-initialized redMap/greenMap/blueMap/whiteMap/white2Map.
♻️ Duplicate comments (1)
src/parlio_p4.cpp (1)
171-203:⚠️ Potential issue | 🟠 MajorLUT null-pointer dereference risk remains — verify
initSuccessgating is sufficient.The past review flagged that
whiteMap,white2Map,redMap,greenMap, andblueMapare dereferenced without null checks. The comment on line 169 states "Requires: LUTs must be validated before calling this function", but the caller (create_transposed_led_output_optimized) doesn't validate them.The mitigation relies on
showPixelsImpl()checkinginitSuccessbefore callingshow_parlio_p4(). However, per context snippet 2 (src/I2SClocklessLedDriver.h:1181-1202), ifsetBrightness()is called standalone after a successfulinitLedImpl()and fails to allocate LUTs, it setsinitErrorOccurred = truebut does not clearinitSuccess. The nextshowPixelsImpl()call will proceed becauseinitSuccessremainstrue.🛡️ Suggested defensive fix in `show_parlio_p4` or `rgbwBufferMapping`
Option 1: Guard in
show_parlio_p4before callingcreate_transposed_led_output_optimized:+ // Defensive: ensure LUT maps are allocated + if (!driver->redMap || !driver->greenMap || !driver->blueMap) { + ESP_LOGE(TAG, "show_parlio_p4: LUT maps not initialized"); + return 3; + } + if (offsetW != UINT8_MAX && !driver->whiteMap) { + ESP_LOGE(TAG, "show_parlio_p4: whiteMap not allocated but offsetW set"); + return 3; + } + if (offsetW2 != UINT8_MAX && !driver->white2Map) { + ESP_LOGE(TAG, "show_parlio_p4: white2Map not allocated but offsetW2 set"); + return 3; + }Option 2: Clear
initSuccessinsetBrightness()on allocation failure (inI2SClocklessLedDriver.h):if (!redMap) { redMap = (uint8_t*)malloc(256); if (!redMap) { ESP_LOGE(TAG, "Failed to allocate redMap!"); initErrorOccurred = true; + initSuccess = false; // Prevent showPixels() from proceeding return; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` around lines 171 - 203, The LUT pointers (whiteMap, white2Map, redMap, greenMap, blueMap) are dereferenced in rgbwBufferMapping (called via show_parlio_p4 -> create_transposed_led_output_optimized) without guaranteeing they are non-null because initSuccess can remain true after setBrightness() fails to allocate LUTs; fix by adding a defensive check: either (A) in setBrightness() clear initSuccess when LUT allocation fails (set initErrorOccurred = true and initSuccess = false) so subsequent showPixelsImpl()/show_parlio_p4 won't call into create_transposed_led_output_optimized, or (B) add null checks before dereferencing each map inside rgbwBufferMapping (and/or pre-check all maps at start of show_parlio_p4 and bail if any are null) to avoid null-pointer derefs; update the code around setBrightness, initLedImpl, rgbwBufferMapping, and show_parlio_p4 accordingly.
🧹 Nitpick comments (2)
src/parlio_p4.cpp (2)
404-412:ESP_ERROR_CHECKwill abort on PARLIO API failures — consider graceful error handling.Using
ESP_ERROR_CHECK()onparlio_tx_unit_wait_all_done,parlio_tx_unit_disable,parlio_del_tx_unit,parlio_new_tx_unit, andparlio_tx_unit_enablewill callabort()if any returns an error. This is appropriate for development but may be harsh in production.Consider whether graceful degradation (log error, return failure code, skip frame) would be preferable for a library intended for diverse use cases.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` around lines 404 - 412, The current use of ESP_ERROR_CHECK around parlio_tx_unit_wait_all_done, parlio_tx_unit_disable, parlio_del_tx_unit, parlio_new_tx_unit, and parlio_tx_unit_enable will abort on any PARLIO error; replace these with explicit return-value checks that log the error (include the returned esp_err_t and context), perform safe cleanup of driver->p4TxUnit (set to NULL after del), and propagate a failure status (e.g., return false or an esp_err_t) instead of aborting; update the surrounding function to handle and propagate failures so callers can degrade gracefully rather than having ESP_ERROR_CHECK call abort().
227-249: Static waveform cache initialization is not thread-safe.The
waveform_cacheis lazily initialized on first use with a staticwaveform_cache_initializedflag. If two tasks callshow_parlio_p4concurrently during startup, there's a potential race condition. However, this is likely acceptable given:
- LED driver calls are typically serialized by application code.
- The cache is idempotent (same values written on re-initialization).
Consider adding a brief comment noting this assumption, or use
std::call_onceif C++11 threading is available.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` around lines 227 - 249, The lazy initialization of the static waveform_cache using waveform_cache_initialized is not thread-safe; either make it thread-safe by replacing the flag with a std::once_flag + std::call_once invocation that initializes waveform_cache (referencing waveform_cache and waveform_cache_initialized or replace the latter) so concurrent calls to show_parlio_p4 cannot race, or, if you accept the race based on idempotence and serialized caller semantics, add a concise comment above the block documenting the assumption (mention waveform_cache, waveform_cache_initialized and the initialization loop) so future readers know why no synchronization is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/developer/standardsandguidelines.md`:
- Around line 69-88: The fenced code block containing the CodeRabbit merge
prompt should include a language specifier (e.g., text or markdown) to satisfy
MD040 and enable proper highlighting; update the block that begins with
"@coderabbitai, I am about to merge this PR..." in
docs/developer/standardsandguidelines.md to use a language-tagged fence (for
example change ``` to ```text) so the linter and renderers treat it correctly.
- Around line 18-39: The fenced codeblock in developer/standardsandguidelines.md
lacks a language specifier which triggers MD040; update the opening
triple-backticks to include a language (e.g., ```text) so the file-tree snippet
(showing entries like I2SClocklessLedDriver.h, I2SClocklessLedDriver.cpp,
parlio_p4.h/.cpp, pixeltypes.h, framebuffer.h, HardwareSprite.h/.cpp, helper.h,
main.cpp) is explicitly marked as plain text for proper syntax highlighting and
markdown lint compliance.
- Around line 207-277: Update the five prompt-pattern fenced code blocks by
adding a language specifier (e.g., "text") to each opening triple-backtick so
they become ```text; specifically update the blocks that begin "Add support for
CONFIG_IDF_TARGET_ESP32XX following the pattern used for ESP32-P4.", "Add an
initled() overload that accepts...", "In [loadAndTranspose / interruptHandler /
i2sStop], the following behaviour is wrong: ...", and the two
documentation/update patterns — ensure each of those prompt pattern blocks (the
ones at the headings for initled(), ISR/transposition, and the two docs-related
examples) uses ```text to satisfy MD040 and enable proper highlighting.
---
Outside diff comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 486-563: The setBrightness() function can set initErrorOccurred on
allocation failure but leaves initSuccess true from a prior init, allowing
showPixelsImpl() to run with null LUTs; update setBrightness() to clear
initSuccess whenever an allocation fails (set initSuccess = false at each error
return) and also proactively clear initSuccess at the start of setBrightness()
(or set initErrorOccurred = false and initSuccess = false before allocations) so
rgbwBufferMapping and showPixelsImpl() never run with partially-initialized
redMap/greenMap/blueMap/whiteMap/white2Map.
---
Duplicate comments:
In `@src/parlio_p4.cpp`:
- Around line 171-203: The LUT pointers (whiteMap, white2Map, redMap, greenMap,
blueMap) are dereferenced in rgbwBufferMapping (called via show_parlio_p4 ->
create_transposed_led_output_optimized) without guaranteeing they are non-null
because initSuccess can remain true after setBrightness() fails to allocate
LUTs; fix by adding a defensive check: either (A) in setBrightness() clear
initSuccess when LUT allocation fails (set initErrorOccurred = true and
initSuccess = false) so subsequent showPixelsImpl()/show_parlio_p4 won't call
into create_transposed_led_output_optimized, or (B) add null checks before
dereferencing each map inside rgbwBufferMapping (and/or pre-check all maps at
start of show_parlio_p4 and bail if any are null) to avoid null-pointer derefs;
update the code around setBrightness, initLedImpl, rgbwBufferMapping, and
show_parlio_p4 accordingly.
---
Nitpick comments:
In `@src/parlio_p4.cpp`:
- Around line 404-412: The current use of ESP_ERROR_CHECK around
parlio_tx_unit_wait_all_done, parlio_tx_unit_disable, parlio_del_tx_unit,
parlio_new_tx_unit, and parlio_tx_unit_enable will abort on any PARLIO error;
replace these with explicit return-value checks that log the error (include the
returned esp_err_t and context), perform safe cleanup of driver->p4TxUnit (set
to NULL after del), and propagate a failure status (e.g., return false or an
esp_err_t) instead of aborting; update the surrounding function to handle and
propagate failures so callers can degrade gracefully rather than having
ESP_ERROR_CHECK call abort().
- Around line 227-249: The lazy initialization of the static waveform_cache
using waveform_cache_initialized is not thread-safe; either make it thread-safe
by replacing the flag with a std::once_flag + std::call_once invocation that
initializes waveform_cache (referencing waveform_cache and
waveform_cache_initialized or replace the latter) so concurrent calls to
show_parlio_p4 cannot race, or, if you accept the race based on idempotence and
serialized caller semantics, add a concise comment above the block documenting
the assumption (mention waveform_cache, waveform_cache_initialized and the
initialization loop) so future readers know why no synchronization is used.
🪄 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: 1c953ba1-9c3d-44c2-b595-ae70510a6c78
📒 Files selected for processing (5)
docs/developer/standardsandguidelines.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/parlio_p4.cppsrc/parlio_p4.h
🚧 Files skipped from review as they are similar to previous changes (1)
- src/parlio_p4.h
Phase 1 complete. Summary of changes: src/I2SClocklessLedDriver.h — 4 renames, replace_all: - i2sInit → hwInit (definition + call in initLedImpl, + commented-out old member) - initDMABuffers → initTransferBuffers (definition + calls in initLedImpl and showPixelsImpl) - i2sStart → hwStart (definition + 2 call sites) - i2sStop → hwStop (static function + 6 call sites in ISR path) src/I2SClocklessLedDriver.cpp — initDMABuffers → initTransferBuffers in updateDriver() docs/developer/developer.md — all occurrences updated; Phase 1 heading marked ✅ done; "Current name" column header changed to "Old name" i2sResetDma and i2sResetFifo were left unchanged — they are ESP32-D0 internal helpers called only from within hwInit(). Additional fixes: - MD040: 7 untagged blocks in standardsandguidelines.md — fix needed - setBrightness() / initSuccess = false: The user is right it's redundant when called from initLedImpl() (which resets initSuccess at the start). But setBrightness() can be called standalone after a successful init — in that case initSuccess stays true with null LUTs. Fix is valid. - LUT null ptr in rgbwBufferMapping: Already has "Requires: LUTs must be validated" precondition comment. Fixing setBrightness() covers this — no additional changes in rgbwBufferMapping. - ESP_ERROR_CHECK: Valid nitpick — replace with explicit error handling. - waveform_cache_initialized thread safety: show_parlio_p4 is always single-threaded; initialization is idempotent — add a comment rather than std::call_once. All diagnostics are the known cross-compilation noise. Summary of all changes made: docs/developer/standardsandguidelines.md — MD040: added text language tag to 7 untagged fenced blocks (file tree, CodeRabbit prompt, 5 prompt-pattern blocks); also updated two stale i2sStop → hwStop references inside prose and prompt text. src/I2SClocklessLedDriver.h — setBrightness(): added initSuccess = false to each of the 5 allocation failure return paths. This prevents showPixelsImpl() from running with null LUTs when setBrightness() fails as a standalone call after a prior successful init. Redundant within initLedImpl() (as you noted), but necessary for the standalone case. src/parlio_p4.cpp: - PARLIO reconfig: replaced all ESP_ERROR_CHECK calls with explicit esp_err_t checks that log the error and return 3 (new failure code) instead of calling abort(). - waveform_cache: added a comment explaining why no synchronization is used (single-task caller, idempotent init).
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (6)
docs/developer/developer.md (1)
217-257: Add language specifiers to pseudocode blocks for better rendering.The markdownlint warnings about missing language specifiers on fenced code blocks (lines 217, 226, 237, 261, 348) are valid. Adding
textorplaintextas the language identifier improves rendering consistency and silences the linter.📝 Example fix for one block
-``` +```text initled(leds, pinsq, sizes[], numStrips, nbComponents, pR, pG, pB, pW, pW2) ← canonical / main🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/developer.md` around lines 217 - 257, The fenced pseudocode blocks in the developer docs (the initled / initLedImpl examples and the cArr decoder description) are missing language specifiers which triggers markdownlint; update each fenced code block containing initled(leds, pinsq, ...), the initLedImpl(...) block, and any other pseudocode snippets mentioning cArr or Pixels to add a language tag such as "text" or "plaintext" (e.g., replace ``` with ```text) so the blocks render consistently and silence the linter.src/parlio_p4.cpp (3)
171-203: LUT null pointer safety concern remains partially unaddressed.While the past review comment was marked as addressed, I notice that
rgbwBufferMapping()still dereferencesdriver->redMap,driver->greenMap, anddriver->blueMapunconditionally (lines 200-202). IfsetBrightness()failed mid-allocation, these could be null.The
initSuccesscheck inshowPixelsImpl()should prevent this path from being reached in normal operation, but a defensive check here would provide defense-in-depth.🛡️ Defensive check for RGB LUTs
static void rgbwBufferMapping(uint8_t* packetRGBChannel, const uint8_t* lightsRGBChannel, const uint8_t offsetRed, const uint8_t offsetGreen, const uint8_t offsetBlue, const uint8_t offsetWhite, const uint8_t offsetWhite2, I2SClocklessLedDriver* driver) { + // Guard: LUTs must be allocated (setBrightness must have succeeded) + if (!driver->redMap || !driver->greenMap || !driver->blueMap) { + memset(packetRGBChannel, 0, 5); // safe fallback: output black + return; + } + uint8_t red = lightsRGBChannel[0];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` around lines 171 - 203, rgbwBufferMapping still dereferences driver->redMap/greenMap/blueMap unsafely; add a defensive null-check at the start of rgbwBufferMapping (or right before using the maps) to ensure the RGB LUT pointers are non-null (e.g., if any of driver->redMap/greenMap/blueMap is null) and handle the error by either returning early or writing safe defaults (zero or passthrough) into packetRGBChannel for the red/green/blue offsets; keep the existing white/white2 handling but only use driver->whiteMap/white2Map if those pointers are also non-null, and document that this protects against partial failures from setBrightness()/failed allocations and complements the initSuccess check in showPixelsImpl.
477-480: Potential division by zero ifbytes_per_pixelis 0.Line 477-480:
const uint16_t max_leds_per_chunk = (bytes_per_pixel > 0) ? (HW_MAX_BYTES / bytes_per_pixel) : 0; const uint8_t num_chunks = (max_leds_per_chunk > 0) ? (uint8_t)((max_leds + max_leds_per_chunk - 1u) / max_leds_per_chunk) : 1u;The
bytes_per_pixel > 0guard is good, but ifmax_leds_per_chunkis 0,num_chunksbecomes 1, and the loop at line 517 will attempt to transmitchunk_bits[0]which would be 0. This edge case (0 components or 0 data_width) is unlikely in practice but the logic is slightly inconsistent.💡 Consider early return for degenerate cases
+ if (bytes_per_pixel == 0 || max_leds == 0) { + ESP_LOGW(TAG, "show_parlio_p4: nothing to transmit (bytes_per_pixel=%u, max_leds=%u)", + (unsigned)bytes_per_pixel, (unsigned)max_leds); + return 0; + } + const uint16_t max_leds_per_chunk = (bytes_per_pixel > 0) ? (HW_MAX_BYTES / bytes_per_pixel) : 0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` around lines 477 - 480, The computation of max_leds_per_chunk and num_chunks can produce a degenerate case when bytes_per_pixel == 0 (making max_leds_per_chunk == 0) which leads to num_chunks == 1 but chunk_bits[0] == 0 and the transmit loop (uses chunk_bits/num_chunks) is invalid; update the logic in the block that defines max_leds_per_chunk and num_chunks to detect the degenerate case (bytes_per_pixel == 0 or max_leds_per_chunk == 0) and handle it explicitly—either return early/skip transmission or set num_chunks to 0 and avoid entering the transmit loop; ensure the fix touches the variables max_leds_per_chunk, num_chunks and the subsequent transmit loop that reads chunk_bits to prevent division by zero or transmitting zero-length chunks.
276-276: VLA on stack may cause stack overflow for large pin counts.Line 276 uses a variable-length array:
uint8_t mappedBuffer[COMPONENTS_PER_PIXEL * SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH];With
COMPONENTS_PER_PIXEL = 5andSOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH = 16, this is only 80 bytes per iteration, which is safe. However, if these constants change in future hardware, the stack usage could grow. Consider documenting the assumption or using a fixed-size buffer.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` at line 276, The local variable-length array mappedBuffer[COMPONENTS_PER_PIXEL * SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH] can overflow the stack if those constants grow; replace it with a safe fixed-size or heap-based container (e.g., std::array with a compile-time max or std::vector<uint8_t> sized at COMPONENTS_PER_PIXEL * SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH) in the function where mappedBuffer is declared, or document the tight size assumption if you intentionally keep the stack allocation; ensure you reference COMPONENTS_PER_PIXEL and SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH when computing the size so the allocation remains correct.src/I2SClocklessLedDriver.cpp (1)
122-137: PARLIO teardown sequence is correct but error logging could be more defensive.The teardown correctly follows the wait → disable → delete sequence matching the pattern in
parlio_p4.cpp(lines 407-416). However, unlike the reconfiguration path inparlio_p4.cppwhich logs errors but continues,deleteDriver()silently ignores return values.Consider logging errors to aid debugging if teardown fails (e.g., due to a timeout or hardware issue), while still proceeding with buffer cleanup.
💡 Optional: Add error logging for PARLIO teardown
`#ifdef` CONFIG_IDF_TARGET_ESP32P4 `#if` HAS_PARLIO_DRIVER if (p4TxUnit != NULL) { - parlio_tx_unit_wait_all_done(p4TxUnit, portMAX_DELAY); - parlio_tx_unit_disable(p4TxUnit); - parlio_del_tx_unit(p4TxUnit); + esp_err_t err; + if ((err = parlio_tx_unit_wait_all_done(p4TxUnit, portMAX_DELAY)) != ESP_OK) + ESP_LOGW(TAG, "deleteDriver: parlio_tx_unit_wait_all_done failed: %s", esp_err_to_name(err)); + if ((err = parlio_tx_unit_disable(p4TxUnit)) != ESP_OK) + ESP_LOGW(TAG, "deleteDriver: parlio_tx_unit_disable failed: %s", esp_err_to_name(err)); + if ((err = parlio_del_tx_unit(p4TxUnit)) != ESP_OK) + ESP_LOGW(TAG, "deleteDriver: parlio_del_tx_unit failed: %s", esp_err_to_name(err)); p4TxUnit = NULL; } `#endif`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.cpp` around lines 122 - 137, The teardown in deleteDriver() currently calls parlio_tx_unit_wait_all_done, parlio_tx_unit_disable and parlio_del_tx_unit without checking or logging their return values; update the p4TxUnit teardown sequence (where p4TxUnit is handled) to capture and log any error/negative return codes from parlio_tx_unit_wait_all_done, parlio_tx_unit_disable and parlio_del_tx_unit (similar to the reconfiguration path in parlio_p4.cpp) while still proceeding with buffer cleanup and resetting p4Buffer1/p4Buffer2/p4BufferActive/initSuccess/p4LastOutputs/p4LastLedsPerOutput; use the project’s existing logging facility to emit contextual error messages indicating which parlio call failed and its return code.src/I2SClocklessLedDriver.h (1)
486-568: LUT allocation failure handling is thorough but has an early-return concern.The defensive allocation checks with
initErrorOccurred = trueare good. However, if an allocation fails mid-way throughsetBrightness()(e.g.,greenMapfails afterredMapsucceeds), the function returns early without filling the already-allocated LUTs. This could leave partially initialized LUTs ifsetBrightness()is called again later.This is a minor edge case since
initErrorOccurredwill prevent further operation, but for robustness consider either:
- Allocating all LUTs first, then filling them, or
- Documenting that
setBrightness()should only be called once during init🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 486 - 568, setBrightness currently allocates LUTs interleaved with early returns, which can leave partially-allocated maps (redMap, greenMap, blueMap, whiteMap, white2Map) if one allocation fails; change the logic to first attempt to allocate all required LUTs (for pW and pW2 check) into the respective pointers (or temporary locals), and if any allocation fails free any maps allocated in this call, set initErrorOccurred = true and initSuccess = false, then return; only after all required allocations succeed proceed to fill the LUTs (the powf loop). Ensure existing whiteMap/white2Map are freed and set to nullptr when pW/pW2 == UINT8_MAX as currently intended.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/developer/developer.md`:
- Around line 89-91: The doc line claiming "No I2S/DMA buffers were allocated,
so nothing is freed." is incorrect for ESP32-P4; update the docs to state that
while the PARLIO unit is managed as static state in parlio_p4.cpp,
deleteDriver() in I2SClocklessLedDriver.cpp does free the ping‑pong waveform
buffers (p4Buffer1 and p4Buffer2) for P4 builds, so revise the paragraph to
mention freeing of those buffers and clarify that the PARLIO unit itself remains
static.
---
Nitpick comments:
In `@docs/developer/developer.md`:
- Around line 217-257: The fenced pseudocode blocks in the developer docs (the
initled / initLedImpl examples and the cArr decoder description) are missing
language specifiers which triggers markdownlint; update each fenced code block
containing initled(leds, pinsq, ...), the initLedImpl(...) block, and any other
pseudocode snippets mentioning cArr or Pixels to add a language tag such as
"text" or "plaintext" (e.g., replace ``` with ```text) so the blocks render
consistently and silence the linter.
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 122-137: The teardown in deleteDriver() currently calls
parlio_tx_unit_wait_all_done, parlio_tx_unit_disable and parlio_del_tx_unit
without checking or logging their return values; update the p4TxUnit teardown
sequence (where p4TxUnit is handled) to capture and log any error/negative
return codes from parlio_tx_unit_wait_all_done, parlio_tx_unit_disable and
parlio_del_tx_unit (similar to the reconfiguration path in parlio_p4.cpp) while
still proceeding with buffer cleanup and resetting
p4Buffer1/p4Buffer2/p4BufferActive/initSuccess/p4LastOutputs/p4LastLedsPerOutput;
use the project’s existing logging facility to emit contextual error messages
indicating which parlio call failed and its return code.
In `@src/I2SClocklessLedDriver.h`:
- Around line 486-568: setBrightness currently allocates LUTs interleaved with
early returns, which can leave partially-allocated maps (redMap, greenMap,
blueMap, whiteMap, white2Map) if one allocation fails; change the logic to first
attempt to allocate all required LUTs (for pW and pW2 check) into the respective
pointers (or temporary locals), and if any allocation fails free any maps
allocated in this call, set initErrorOccurred = true and initSuccess = false,
then return; only after all required allocations succeed proceed to fill the
LUTs (the powf loop). Ensure existing whiteMap/white2Map are freed and set to
nullptr when pW/pW2 == UINT8_MAX as currently intended.
In `@src/parlio_p4.cpp`:
- Around line 171-203: rgbwBufferMapping still dereferences
driver->redMap/greenMap/blueMap unsafely; add a defensive null-check at the
start of rgbwBufferMapping (or right before using the maps) to ensure the RGB
LUT pointers are non-null (e.g., if any of driver->redMap/greenMap/blueMap is
null) and handle the error by either returning early or writing safe defaults
(zero or passthrough) into packetRGBChannel for the red/green/blue offsets; keep
the existing white/white2 handling but only use driver->whiteMap/white2Map if
those pointers are also non-null, and document that this protects against
partial failures from setBrightness()/failed allocations and complements the
initSuccess check in showPixelsImpl.
- Around line 477-480: The computation of max_leds_per_chunk and num_chunks can
produce a degenerate case when bytes_per_pixel == 0 (making max_leds_per_chunk
== 0) which leads to num_chunks == 1 but chunk_bits[0] == 0 and the transmit
loop (uses chunk_bits/num_chunks) is invalid; update the logic in the block that
defines max_leds_per_chunk and num_chunks to detect the degenerate case
(bytes_per_pixel == 0 or max_leds_per_chunk == 0) and handle it
explicitly—either return early/skip transmission or set num_chunks to 0 and
avoid entering the transmit loop; ensure the fix touches the variables
max_leds_per_chunk, num_chunks and the subsequent transmit loop that reads
chunk_bits to prevent division by zero or transmitting zero-length chunks.
- Line 276: The local variable-length array mappedBuffer[COMPONENTS_PER_PIXEL *
SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH] can overflow the stack if those constants
grow; replace it with a safe fixed-size or heap-based container (e.g.,
std::array with a compile-time max or std::vector<uint8_t> sized at
COMPONENTS_PER_PIXEL * SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH) in the function where
mappedBuffer is declared, or document the tight size assumption if you
intentionally keep the stack allocation; ensure you reference
COMPONENTS_PER_PIXEL and SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH when computing the
size so the allocation remains correct.
🪄 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: 63e38413-c5e3-44f2-8c01-dc5f60bf4f04
⛔ Files ignored due to path filters (1)
docs/developer/PRflow.pngis excluded by!**/*.png
📒 Files selected for processing (5)
docs/developer/developer.mddocs/developer/standardsandguidelines.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/parlio_p4.cpp
✅ Files skipped from review due to trivial changes (1)
- docs/developer/standardsandguidelines.md
┌────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────┐ │ Finding │ Change │ ├────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ developer.md L83 stale p4Pins[] │ Updated to pins[]; corrected initLedImpl P4 description to mention buffer allocation │ ├────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ developer.md L89-91 incorrect deleteDriver │ Rewrote to state PARLIO unit + p4Buffer1/2 are freed │ ├────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ developer.md L217-257 MD040 │ Added text tag to 5 pseudocode blocks │ ├────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ I2SClocklessLedDriver.cpp teardown │ Added esp_err_t checks + ESP_LOGE on all 3 PARLIO teardown calls │ ├────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ parlio_p4.cpp rgbwBufferMapping null guard │ Added null check for redMap/greenMap/blueMap; zeroes output and returns early │ ├────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤ │ parlio_p4.cpp bytes_per_pixel == 0 │ Added explicit early-return guard; simplified num_chunks (no longer needs ? : 1u) │ └────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────┘ except: Confirmed: null LUTs cannot reach rgbwBufferMapping because: - Every setBrightness() allocation failure sets initSuccess = false - showPixelsImpl() returns immediately if !initSuccess (line 1191), before any P4 or LUT code runs - rgbwBufferMapping is only reachable via create_transposed_led_output_optimized → show_parlio_p4 → showPixelsImpl phase 2: Phase 2 complete. Summary: src/colorarrangement.h — new file containing: - ColorArrangement enum (moved verbatim from I2SClocklessLedDriver.h) - applyColorArrangement() inline free function with the switch(cArr) body (initialises pW/pW2 to UINT8_MAX then sets all offsets per enum value) src/I2SClocklessLedDriver.h: - ColorArrangement enum replaced with #include "colorarrangement.h" - Canonical initled() (explicit pR/pG/pB/pW/pW2) promoted with a full doc-block marking it as the primary entry point - cArr-taking overload (variable lengths) reduced from 60 lines to 4: calls applyColorArrangement() then delegates to canonical — no more duplicated switch - Uniform-length cArr overload already delegated to the variable-length one, so it transitively uses the canonical with zero changes Phase 3 complete. Summary of changes: src/parlio_p4.h — replaced the single show_parlio_p4() declaration with 5 documented declarations: initTransferBuffers, hwInit, loadAndTranspose, hwStart, hwStop. src/parlio_p4.cpp — show_parlio_p4 split into 5 functions: - initTransferBuffers — allocates p4Buffer1/2, sets p4LastOutputs=-1 to force hwInit on next call - hwInit — lazy topology check; returns true = skip warm-up frame, false = no change, proceed. Reads driver->pins[] directly instead of taking a parallelPins parameter - loadAndTranspose — buffer capacity guard + create_transposed_led_output_optimized. Reads all params from driver directly (no parameter list) - hwStart — captures chunk ptrs from pre-swap buffer, swaps ping-pong, queues non-blocking PARLIO TX - hwStop — wait_all_done + timing guard delay src/I2SClocklessLedDriver.h: - initLedImpl P4 branch: inline buffer alloc (30 lines) → initTransferBuffers(this) (1 line) - showPixelsImpl P4 branch: show_parlio_p4(this, pins, ...) → hwInit + loadAndTranspose + hwStart + hwStop
|
Note Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
✅ Created PR with unit tests: #55 |
|
Note Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
✅ Created PR with unit tests: #56 |
|
@coderabbitai resume and review |
|
🧠 Learnings used✅ Actions performedReviews resumed. Review triggered.
|
|
@coderabbitai, is your latest review of 1ec96ff in progress? (I don't see a pending coderabbit review) |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/I2SClocklessLedDriver.h (1)
1682-1715:⚠️ Potential issue | 🔴 CriticalCritical: Static
hwStopdefinition conflicts with P4 free function.This
staticfunction definition conflicts with theexterndeclaration inparlio_p4.h:63. The static function is only needed for ESP32/S3 builds.🐛 Proposed fix
+#if !defined(CONFIG_IDF_TARGET_ESP32P4) static void IRAM_ATTR hwStop(I2SClocklessLedDriver* cont) { `#ifdef` CONFIG_IDF_TARGET_ESP32S3 ... `#endif` } +#endif🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 1682 - 1715, The function hwStop currently defined as "static void IRAM_ATTR hwStop(I2SClocklessLedDriver* cont)" conflicts with the extern declaration in parlio_p4.h; change its linkage to external by removing the static keyword (make it "void IRAM_ATTR hwStop(I2SClocklessLedDriver* cont)") while keeping the existing target-specific `#ifdef` blocks so the implementation is only compiled for ESP targets; update the declaration usage around symbols like I2SClocklessLedDriver, hwStop, cont->i2sReset, cont->wasWaitingtofinish and cont->waitDisp to ensure the non-static definition matches the extern in parlio_p4.h.
🧹 Nitpick comments (2)
src/parlio_p4.cpp (1)
44-106: Consider addingIRAM_ATTRtotranspose_32_slicesfor ISR-path consistency.While the P4 path doesn't use ISR-based transposition (it's synchronous), the
__attribute__((hot))on the calling functions suggests this is a hot path. For consistency with ESP32/S3 code style, consider whetherIRAM_ATTRwould benefit P4 (it may be less critical since P4 has more cache).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.cpp` around lines 44 - 106, The transpose_32_slices function is a hot path and reviewer suggests marking it for ISR-path/IRAM placement; add the ESP32 IRAM_ATTR attribute to transpose_32_slices to ensure it is placed in IRAM (e.g., change the declaration/definition of transpose_32_slices to include IRAM_ATTR before the return type) so it matches the calling hot functions' intent and avoids flash fetch latency on ISR-like execution paths.docs/developer/developer.md (1)
314-314: Nit: Mixed spelling variants "optimize" vs "optimise".The documentation uses both "optimized" and references to function names with "optimized". For consistency across the documentation, consider standardizing on one variant (typically "optimize" for code-facing docs in US English codebases).
This is purely cosmetic and doesn't affect functionality.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/developer.md` at line 314, Docs use mixed "optimise/optimised" vs "optimize/optimized"; standardize to one variant (prefer US "optimize/optimized") across the doc. Search for the symbols loadAndTranspose and create_transposed_led_output_optimized and update any occurrences that use the British spelling (e.g., "optimise"/"optimised") to the chosen variant so table entries, function references, and surrounding text are consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 1475-1486: The P4-specific path is incorrectly calling the
zero-arg member initTransferBuffers() with this (initTransferBuffers(this))
causing a signature mismatch; fix by invoking the free function declared in
parlio_p4.h instead of the member: ensure parlio_p4.h is included in this
translation unit and resolve linkage conflicts by making any internal/static
helpers in parlio_p4.h static/inline or namespace-scoped so the free function
bool initTransferBuffers(I2SClocklessLedDriver*) is visible; then in the P4
block call the free function (initTransferBuffers(this)) while the non-P4 path
continues to call the member initTransferBuffers(), and keep existing
setPins(pinsq), hwInit(), and initSuccess logic unchanged.
- Line 246: The static forward declaration "static void
loadAndTranspose(I2SClocklessLedDriver* driver);" conflicts with the extern in
parlio_p4.h; remove or guard it so it only exists for ESP32/S3 builds: either
delete this header-level static declaration and keep the P4 extern, or wrap the
declaration of loadAndTranspose in an appropriate preprocessor guard (e.g., the
ESP32/S3 target macro used in this project) so the static symbol is compiled
only for ESP32/S3; ensure the function definition remains consistent with the
chosen visibility (static vs extern) and update callers in I2SClocklessLedDriver
that refer to loadAndTranspose accordingly.
- Around line 1191-1202: The P4 build is calling free functions from parlio_p4.h
(hwInit, loadAndTranspose, hwStart, hwStop) but this header also defines member
functions with the same names/signatures and local static functions shadowing
them; fix by removing or `#ifdef-guarding` the static/free-function declarations
in this header for non-P4 builds so the parlio_p4.h free functions are visible
for CONFIG_IDF_TARGET_ESP32P4; specifically adjust the declarations/definitions
for loadAndTranspose, hwStart, hwStop and ensure hwInit and initTransferBuffers
calls in the CONFIG_IDF_TARGET_ESP32P4 block resolve to the parlio_p4.h free
functions rather than the class members (or rename local statics) so the
signatures match.
In `@src/parlio_p4.cpp`:
- Around line 453-476: The build error is caused by two conflicting definitions
of loadAndTranspose: the static IRAM_ATTR loadAndTranspose in
I2SClocklessLedDriver.h and this non-static
loadAndTranspose(I2SClocklessLedDriver*) in src/parlio_p4.cpp. Fix by wrapping
the header's declaration/definition of static void IRAM_ATTR
loadAndTranspose(...) in a preprocessor guard so it is excluded for P4 targets
(e.g. surround the header's static loadAndTranspose with `#if`
!defined(CONFIG_IDF_TARGET_ESP32P4) ... `#endif`) so the P4-specific
implementation in parlio_p4.cpp remains the sole definition.
- Around line 532-539: The hwStop function in src/parlio_p4.cpp conflicts with a
static definition of hwStop (static void IRAM_ATTR hwStop()) in
I2SClocklessLedDriver.h; fix by conditionally compiling the header's static
hwStop so it is excluded for P4 builds: wrap the header's static hwStop
definition in a preprocessor guard that disables it when the P4 implementation
is enabled (e.g., `#if` !defined(PARLIO_P4) or the project's existing P4 build
macro), ensuring only the P4-specific hwStop in parlio_p4.cpp remains for P4
builds.
In `@src/parlio_p4.h`:
- Around line 34-63: The declaration linkage for loadAndTranspose and hwStop in
parlio_p4.h conflicts with static definitions of the same symbols in
I2SClocklessLedDriver.h; fix by making the non-P4 declarations/definitions
conditional so they are excluded when compiling the P4-specific code. Update
I2SClocklessLedDriver.h to wrap the static definitions/declarations of
loadAndTranspose and hwStop (and any other P4-conflicting static helpers) in the
inverse of the P4 build macro used in the project (e.g., `#ifndef`
<P4_BUILD_MACRO>) so the symbols are only static for non-P4 builds, or
alternatively change the parlio_p4.h prototypes to match the same
static/internal linkage; ensure the unique symbols loadAndTranspose and hwStop
are no longer declared extern in one header and static in the other.
---
Outside diff comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 1682-1715: The function hwStop currently defined as "static void
IRAM_ATTR hwStop(I2SClocklessLedDriver* cont)" conflicts with the extern
declaration in parlio_p4.h; change its linkage to external by removing the
static keyword (make it "void IRAM_ATTR hwStop(I2SClocklessLedDriver* cont)")
while keeping the existing target-specific `#ifdef` blocks so the implementation
is only compiled for ESP targets; update the declaration usage around symbols
like I2SClocklessLedDriver, hwStop, cont->i2sReset, cont->wasWaitingtofinish and
cont->waitDisp to ensure the non-static definition matches the extern in
parlio_p4.h.
---
Nitpick comments:
In `@docs/developer/developer.md`:
- Line 314: Docs use mixed "optimise/optimised" vs "optimize/optimized";
standardize to one variant (prefer US "optimize/optimized") across the doc.
Search for the symbols loadAndTranspose and
create_transposed_led_output_optimized and update any occurrences that use the
British spelling (e.g., "optimise"/"optimised") to the chosen variant so table
entries, function references, and surrounding text are consistent.
In `@src/parlio_p4.cpp`:
- Around line 44-106: The transpose_32_slices function is a hot path and
reviewer suggests marking it for ISR-path/IRAM placement; add the ESP32
IRAM_ATTR attribute to transpose_32_slices to ensure it is placed in IRAM (e.g.,
change the declaration/definition of transpose_32_slices to include IRAM_ATTR
before the return type) so it matches the calling hot functions' intent and
avoids flash fetch latency on ISR-like execution paths.
🪄 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: 7c4378aa-44ae-4366-b043-724e81079dee
⛔ Files ignored due to path filters (1)
docs/developer/PRflow.pngis excluded by!**/*.png
📒 Files selected for processing (7)
docs/developer/developer.mddocs/developer/standardsandguidelines.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/colorarrangement.hsrc/parlio_p4.cppsrc/parlio_p4.h
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 77318
🧠 Learnings used |
|
@coderabbitai, regarding
The function is currently inline void transpose_32_slices(...) — no IRAM_ATTR. The suggestion doesn't need to be applied for two reasons:
|
|
Tip For best results, initiate chat on the files or code changes.
The suggestion was incorrect and should be disregarded. I've noted this for future reviews. ✏️ Learnings added
🧠 Learnings used |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
docs/developer/standardsandguidelines.md (1)
75-75: Self-referential PR example.The documentation uses PR
#54(this PR) as an example, creating a circular reference. Consider using a different, already-merged PR as the example to avoid confusion for future readers.📝 Suggested alternative
-- see [this PR](https://github.com/hpwit/I2SClocklessLedDriver/pull/54) : Coderabbit has done it's reviews and additional commits are done +- see [an example PR](https://github.com/hpwit/I2SClocklessLedDriver/pull/51) : Coderabbit has done its reviews and additional commits are done(Replace PR
#54with a completed PR that demonstrates the workflow, or use a generic placeholder if this is a template.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/standardsandguidelines.md` at line 75, The line referencing "PR `#54`" in the standardsandguidelines.md example creates a circular, self-referential example; replace that specific reference ("PR `#54`" and the sentence "see [this PR]...") with a reference to a completed, already-merged PR (or a generic placeholder like "PR #<example>" or "a completed PR example") so the document demonstrates the intended workflow without pointing to the current PR.src/I2SClocklessLedDriver.h (1)
1455-1489: Minor: Consider early return ifsetBrightnessfails.If
setBrightness(255)at line 1455 fails (allocation error), it setsinitErrorOccurred = truebutinitLedImplcontinues tosetPins,initTransferBuffers, etc. The finalinitSuccesscheck correctly sets false, but intermediate allocations are wasteful.♻️ Optional improvement to fail fast
setBrightness(255); + if (initErrorOccurred) return; // LUT allocation failed — stop early /* ... */ `#ifdef` CONFIG_IDF_TARGET_ESP32P4🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 1455 - 1489, The call to setBrightness(255) can set initErrorOccurred on failure but code continues doing costly setup; change initLedImpl so setBrightness's result (or initErrorOccurred) is checked immediately and return early when it fails: after calling setBrightness(255) check whether it failed (initErrorOccurred true or setBrightness returned false) and if so skip the later calls to setPins, hwInit, initTransferBuffers and set initSuccess = false; apply the same early-return behavior for both CONFIG_IDF_TARGET_ESP32P4 and non-P4 branches so setPins, initTransferBuffers, hwInit are not executed when setBrightness failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 377-393: The functions loadAndTranspose, hwStart, and hwStop
reference PARLIO-specific members (p4Config, p4TxUnit, p4BufferActive etc.) that
are only declared when HAS_PARLIO_DRIVER is enabled; to avoid compile errors
when HAS_PARLIO_DRIVER=0, wrap the entire definitions of loadAndTranspose,
hwStart, and hwStop in `#if` HAS_PARLIO_DRIVER / `#endif` so those functions (which
access driver->p4Config.data_width, driver->p4Config.max_transfer_size,
driver->p4TxUnit, etc.) are only compiled when PARLIO symbols exist.
---
Nitpick comments:
In `@docs/developer/standardsandguidelines.md`:
- Line 75: The line referencing "PR `#54`" in the standardsandguidelines.md
example creates a circular, self-referential example; replace that specific
reference ("PR `#54`" and the sentence "see [this PR]...") with a reference to a
completed, already-merged PR (or a generic placeholder like "PR #<example>" or
"a completed PR example") so the document demonstrates the intended workflow
without pointing to the current PR.
In `@src/I2SClocklessLedDriver.h`:
- Around line 1455-1489: The call to setBrightness(255) can set
initErrorOccurred on failure but code continues doing costly setup; change
initLedImpl so setBrightness's result (or initErrorOccurred) is checked
immediately and return early when it fails: after calling setBrightness(255)
check whether it failed (initErrorOccurred true or setBrightness returned false)
and if so skip the later calls to setPins, hwInit, initTransferBuffers and set
initSuccess = false; apply the same early-return behavior for both
CONFIG_IDF_TARGET_ESP32P4 and non-P4 branches so setPins, initTransferBuffers,
hwInit are not executed when setBrightness failed.
🪄 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: d955253f-d645-4d2c-b340-2b4a4bd5bea9
📒 Files selected for processing (4)
docs/developer/developer.mddocs/developer/standardsandguidelines.mddocs/developer/workinprogress.mdsrc/I2SClocklessLedDriver.h
✅ Files skipped from review due to trivial changes (2)
- docs/developer/workinprogress.md
- docs/developer/developer.md
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/parlio_p4.h (1)
43-48: Misleading documentation:hwInit()isvoid, notbool.The comment states "Must only be called after hwInit() returns false" but
hwInit()has no return value (it'svoid). Consider clarifying to something like "Must only be called after hwInit() has completed" or "Must only be called after successful initialization."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/parlio_p4.h` around lines 43 - 48, The comment for loadAndTranspose incorrectly says "Must only be called after hwInit() returns false" even though hwInit() is void; update the docstring associated with loadAndTranspose to remove the return-value phrasing and instead state a correct condition such as "Must only be called after hwInit() has completed" or "Must only be called after successful initialization." Ensure you edit the comment above void loadAndTranspose(I2SClocklessLedDriver* driver); and keep the rest of the description about bit-transpose and LUT application unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 33-58: The P4 branch in updateDriver calls ::hwInit(this) but does
not mark failures, so if ::hwInit sets p4TxUnit=null (or fails) we must
propagate that to the driver state: after calling ::hwInit(this) check its
failure condition (e.g., p4TxUnit == NULL or other hwInit return/error flag) and
set this->initErrorOccurred = true (or driver->initErrorOccurred) and avoid
calling setBrightness()/return early or handle accordingly; ensure updateDriver
(P4 path) does not call setBrightness or leave the driver appearing healthy when
::hwInit failed so subsequent showPixels/::hwStart cannot dereference a NULL
p4TxUnit.
In `@src/I2SClocklessLedDriver.h`:
- Around line 1473-1480: initLedImpl() currently calls ::hwInit(this) but
doesn't set initErrorOccurred or handle failure, so a NULL p4TxUnit can lead to
initSuccess being true; modify initLedImpl() to check the return/state of
::hwInit(this) (or verify p4TxUnit afterwards) and if hwInit failed set
initErrorOccurred = true and avoid setting initSuccess; ensure the existing
early-return pattern used for initTransferBuffers() is mirrored here (after
setPins(pinsq) call), so that on hwInit failure you return early and do not mark
initSuccess.
---
Nitpick comments:
In `@src/parlio_p4.h`:
- Around line 43-48: The comment for loadAndTranspose incorrectly says "Must
only be called after hwInit() returns false" even though hwInit() is void;
update the docstring associated with loadAndTranspose to remove the return-value
phrasing and instead state a correct condition such as "Must only be called
after hwInit() has completed" or "Must only be called after successful
initialization." Ensure you edit the comment above void
loadAndTranspose(I2SClocklessLedDriver* driver); and keep the rest of the
description about bit-transpose and LUT application unchanged.
🪄 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: 70916910-2e75-4b7b-acd6-0060d053297a
📒 Files selected for processing (5)
docs/developer/workinprogress.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/parlio_p4.cppsrc/parlio_p4.h
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/developer/workinprogress.md
- src/parlio_p4.cpp
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/I2SClocklessLedDriver.h (2)
1446-1466:⚠️ Potential issue | 🔴 CriticalValidate
channelsPerLightand channel indices at the API boundary.This overload accepts arbitrary
channelsPerLight,pR,pG,pB,pW, andpW2, but downstream code assumes3 <= channelsPerLight <= 5and writes into fixed-size buffers likemapped[5]. PassingchannelsPerLight > 5or an index>= channelsPerLightwill walk past those arrays.🛠️ Proposed fix
void initled(uint8_t* leds, uint8_t* pinsq, uint16_t* sizes, uint8_t numStrips, uint8_t channelsPerLight, uint8_t pR, uint8_t pG, uint8_t pB, uint8_t pW = UINT8_MAX, uint8_t pW2 = UINT8_MAX, bool extractWhiteFromRGB = false) { if (pinsq == nullptr || sizes == nullptr || numStrips == 0 || numStrips > MAX_PINS) { ESP_LOGE(TAG, "initled: invalid args numStrips=%u sizes=%p pinsq=%p", numStrips, (void*)sizes, (void*)pinsq); return; } + if (channelsPerLight < 3 || channelsPerLight > 5) { + ESP_LOGE(TAG, "initled: unsupported channelsPerLight=%u", channelsPerLight); + return; + } + auto valid_index = [channelsPerLight](uint8_t idx) { + return idx == UINT8_MAX || idx < channelsPerLight; + }; + if (!valid_index(pR) || !valid_index(pG) || !valid_index(pB) || + !valid_index(pW) || !valid_index(pW2)) { + ESP_LOGE(TAG, "initled: invalid channel mapping for channelsPerLight=%u", channelsPerLight); + return; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 1446 - 1466, The initled overload must validate channelsPerLight and the provided channel indices before using fixed-size buffers (e.g., mapped[5]) or calling initLedImpl: add a guard in initled that ensures 3 <= channelsPerLight <= 5 and that pR, pG, pB, pW, pW2 (when not UINT8_MAX) are all < channelsPerLight; on violation, ESP_LOGE with details and return; update any callers/comments to note the constraint and then call initLedImpl only after these checks so downstream code that uses mapped[5] cannot overflow.
707-728:⚠️ Potential issue | 🔴 CriticalPropagate GDMA setup failures on the S3 path.
This block ignores the return values from
gdma_new_*,gdma_connect(),gdma_apply_strategy(), andgdma_register_tx_event_callbacks(). Any one of those can fail and still leaveinitSuccesstrue, which turns the nexthwStart()into a null-handle or partially-initialized GDMA crash.As per coding guidelines, for S3 platform the driver must use the LCD_CAM peripheral and GDMA for I2S operations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 707 - 728, The GDMA setup calls in hwInit (gdma_new_ahb_channel / gdma_new_channel, gdma_connect, gdma_apply_strategy, gdma_register_tx_event_callbacks) are unchecked on the S3 path and can leave initSuccess true despite failures; update hwInit to check each return value and set initSuccess = false (and clean up any partially-created handles) if any call fails so hwStart cannot run with a null/partially-initialized gdmaChan. Specifically, after calling gdma_new_ahb_channel/gdma_new_channel, verify the returned esp_err_t (or bool) and on error set initSuccess=false and free/clear gdmaChan; do the same for gdma_connect, gdma_apply_strategy (using the gdma_strategy_config_t), and gdma_register_tx_event_callbacks (txCbs), propagating errors out of hwInit so callers know initialization failed.src/I2SClocklessLedDriver.cpp (1)
60-96:⚠️ Potential issue | 🔴 CriticalRe-run
hwInit()afterdeleteDriver()before rebuilding buffers.
deleteDriver()tears down the ESP32 interrupt handle and the S3 GDMA channel, butupdateDriver()now goes straight fromsetPins()toinitTransferBuffers(). After a successful reconfiguration,initSuccesscan become true whileintrHandle/dmaChanare still null, so the nextshowPixels()fails inhwStart().🛠️ Proposed fix
setShowDelay(); setPins(pinsq); + hwInit(); if (initErrorOccurred) { initSuccess = false; return; } initTransferBuffers();As per coding guidelines, for S3 platform use LCD_CAM + GDMA, and for ESP32 use I2S0 +
esp_intr_alloc.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.cpp` around lines 60 - 96, After calling deleteDriver() in updateDriver()/constructor, call hwInit() to reinitialize platform HW resources (so intrHandle/dmaChan are recreated) before proceeding to setPins() and initTransferBuffers(); ensure initSuccess is only set true after hwInit() completes successfully and before returning. Update flow around deleteDriver(), hwInit(), setPins(), initTransferBuffers() and the final initSuccess assignment; reference functions/vars: deleteDriver(), hwInit(), updateDriver(), setPins(), initTransferBuffers(), initSuccess, intrHandle, dmaChan, showPixels(), hwStart() and ensure platform-specific init uses LCD_CAM+GDMA for S3 and I2S0 + esp_intr_alloc for ESP32.
♻️ Duplicate comments (3)
src/esp32-d0s3_i2s_impl.h (1)
239-246:⚠️ Potential issue | 🟠 MajorRemove the second
semrelease afterhwStop(driver).
hwStop(driver)already givesdriver->semwhendriver->isWaitingis set. The extra give here bumps the semaphore twice for one frame completion, which can let a laterWAITtake succeed immediately.🛠️ Proposed fix
if (GET_PERI_REG_BITS(I2S_INT_ST_REG(I2S_DEVICE), I2S_OUT_TOTAL_EOF_INT_ST_S, I2S_OUT_TOTAL_EOF_INT_ST_S)) { // ((I2SClocklessLedDriver *)arg)->hwStop(); hwStop(driver); - if (driver->isWaiting) { - portBASE_TYPE hpTaskAwoken = 0; - xSemaphoreGiveFromISR(driver->sem, &hpTaskAwoken); - if (hpTaskAwoken == pdTRUE) portYIELD_FROM_ISR(); - } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/esp32-d0s3_i2s_impl.h` around lines 239 - 246, The code is releasing driver->sem twice on I2S_OUT_TOTAL_EOF interrupt: hwStop(driver) already gives the semaphore when driver->isWaiting is true, so remove the extra xSemaphoreGiveFromISR(driver->sem, &hpTaskAwoken) / portYIELD_FROM_ISR() block after calling hwStop(driver); instead rely on hwStop(driver) to signal the waiting task; keep the hpTaskAwoken logic only if you must give the semaphore here (but per review remove it), and ensure references to hwStop, driver->isWaiting, driver->sem and xSemaphoreGiveFromISR are the only locations changed.src/I2SClocklessLedDriver.h (2)
772-779:⚠️ Potential issue | 🔴 CriticalStop immediately when a transfer buffer allocation fails.
allocateDMABuffer()already returnsNULLand setsinitErrorOccurred, but this loop keeps going and Lines 777-778 dereferencetransferBuffers[i]->bufferunconditionally. That turns an OOM into a hard crash during init.🛠️ Proposed fix
for (int i = 0; i < nbDmaBuffer + 1; i++) { transferBuffers[i] = allocateDMABuffer(channelsPerLight * 8 * 2 * 3); + if (!transferBuffers[i]) { + return; + } } transferBuffers[nbDmaBuffer + 1] = allocateDMABuffer(channelsPerLight * 8 * 2 * 3 * 4); + if (!transferBuffers[nbDmaBuffer + 1]) { + return; + } for (int i = 0; i < nbDmaBuffer; i++) { putdefaultones((uint16_t*)transferBuffers[i]->buffer);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 772 - 779, The allocation loop currently continues on NULL from allocateDMABuffer and later unconditionally dereferences transferBuffers[i]->buffer in putdefaultones; change it to check each allocation return value (allocateDMABuffer) immediately, and if NULL (initErrorOccurred is set), stop initialization: break or return from the init function and avoid any further use of transferBuffers (including skipping the oversized allocation for transferBuffers[nbDmaBuffer + 1] and the subsequent putdefaultones loop). Ensure putdefaultones is only called for buffers that were successfully allocated (check transferBuffers[i] != NULL) and propagate/return the error state so init doesn't crash when out-of-memory.
1318-1333:⚠️ Potential issue | 🟠 MajorOnly release
waitDispwhen a waiter is actually pending.These early exits now give
waitDispunconditionally. BecausewaitDispis a counting semaphore, that leaves stale tokens behind; the next caller enteringwaitDisplay()can consume an old token and skip waiting even though a frame is still in progress.🛠️ Proposed fix
if (!enableDriver) { isDisplaying = false; - if (waitDisp != NULL) xSemaphoreGive(waitDisp); + if (wasWaitingtofinish && waitDisp != NULL) { + wasWaitingtofinish = false; + xSemaphoreGive(waitDisp); + } return; } if (!initSuccess) { isDisplaying = false; - if (waitDisp != NULL) xSemaphoreGive(waitDisp); + if (wasWaitingtofinish && waitDisp != NULL) { + wasWaitingtofinish = false; + xSemaphoreGive(waitDisp); + } return; } if (leds == NULL) { ESP_LOGE(TAG, "no leds buffer defined"); isDisplaying = false; - if (waitDisp != NULL) xSemaphoreGive(waitDisp); + if (wasWaitingtofinish && waitDisp != NULL) { + wasWaitingtofinish = false; + xSemaphoreGive(waitDisp); + } return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 1318 - 1333, The early-return branches (checking enableDriver, initSuccess, leds) unconditionally call xSemaphoreGive(waitDisp) which creates stale tokens; change them to only call xSemaphoreGive when a waiter is actually pending on waitDisp (i.e. detect a pending waiter instead of always giving). Update the code around waitDisp (used by waitDisplay()) to check the semaphore/queue waiting count (e.g. use the FreeRTOS API such as uxQueueMessagesWaiting/uxSemaphoreGetCount or the appropriate waiter-count helper available in this codebase) and only call xSemaphoreGive(waitDisp) when that check indicates a task is waiting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/developer/standardsandguidelines.md`:
- Around line 12-14: The "No mutable globals" guideline conflicts with the
repo's contract: keep NUM_STRIPS, __NB_DMA_BUFFER, and __delay as runtime
globals in src/I2SClocklessLedDriver.cpp and expose them via extern in the
header; update the standards text to explicitly allow these three runtime
globals and reference I2SClocklessLedDriver as the place state should otherwise
live, clarifying that no other file-scope mutable globals are permitted and that
these three symbols are the approved exceptions implemented in the .cpp and
declared extern in the .h.
In `@src/I2SClocklessLedDriver.h`:
- Around line 663-667: Check the return value (esp_err_t e) from esp_intr_alloc
in the code that sets intrHandle and handle failure: if e != ESP_OK, log/record
the error, ensure intrHandle remains nullptr, set initSuccess to false (or
return false) and do not proceed to call initTransferBuffers() or allow
hwStart() to call esp_intr_enable(intrHandle); only mark init successful and
continue when esp_intr_alloc returns ESP_OK. Reference esp_intr_alloc,
intrHandle, initTransferBuffers(), initSuccess, hwStart(), and esp_intr_enable
in your fix.
---
Outside diff comments:
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 60-96: After calling deleteDriver() in updateDriver()/constructor,
call hwInit() to reinitialize platform HW resources (so intrHandle/dmaChan are
recreated) before proceeding to setPins() and initTransferBuffers(); ensure
initSuccess is only set true after hwInit() completes successfully and before
returning. Update flow around deleteDriver(), hwInit(), setPins(),
initTransferBuffers() and the final initSuccess assignment; reference
functions/vars: deleteDriver(), hwInit(), updateDriver(), setPins(),
initTransferBuffers(), initSuccess, intrHandle, dmaChan, showPixels(), hwStart()
and ensure platform-specific init uses LCD_CAM+GDMA for S3 and I2S0 +
esp_intr_alloc for ESP32.
In `@src/I2SClocklessLedDriver.h`:
- Around line 1446-1466: The initled overload must validate channelsPerLight and
the provided channel indices before using fixed-size buffers (e.g., mapped[5])
or calling initLedImpl: add a guard in initled that ensures 3 <=
channelsPerLight <= 5 and that pR, pG, pB, pW, pW2 (when not UINT8_MAX) are all
< channelsPerLight; on violation, ESP_LOGE with details and return; update any
callers/comments to note the constraint and then call initLedImpl only after
these checks so downstream code that uses mapped[5] cannot overflow.
- Around line 707-728: The GDMA setup calls in hwInit (gdma_new_ahb_channel /
gdma_new_channel, gdma_connect, gdma_apply_strategy,
gdma_register_tx_event_callbacks) are unchecked on the S3 path and can leave
initSuccess true despite failures; update hwInit to check each return value and
set initSuccess = false (and clean up any partially-created handles) if any call
fails so hwStart cannot run with a null/partially-initialized gdmaChan.
Specifically, after calling gdma_new_ahb_channel/gdma_new_channel, verify the
returned esp_err_t (or bool) and on error set initSuccess=false and free/clear
gdmaChan; do the same for gdma_connect, gdma_apply_strategy (using the
gdma_strategy_config_t), and gdma_register_tx_event_callbacks (txCbs),
propagating errors out of hwInit so callers know initialization failed.
---
Duplicate comments:
In `@src/esp32-d0s3_i2s_impl.h`:
- Around line 239-246: The code is releasing driver->sem twice on
I2S_OUT_TOTAL_EOF interrupt: hwStop(driver) already gives the semaphore when
driver->isWaiting is true, so remove the extra
xSemaphoreGiveFromISR(driver->sem, &hpTaskAwoken) / portYIELD_FROM_ISR() block
after calling hwStop(driver); instead rely on hwStop(driver) to signal the
waiting task; keep the hpTaskAwoken logic only if you must give the semaphore
here (but per review remove it), and ensure references to hwStop,
driver->isWaiting, driver->sem and xSemaphoreGiveFromISR are the only locations
changed.
In `@src/I2SClocklessLedDriver.h`:
- Around line 772-779: The allocation loop currently continues on NULL from
allocateDMABuffer and later unconditionally dereferences
transferBuffers[i]->buffer in putdefaultones; change it to check each allocation
return value (allocateDMABuffer) immediately, and if NULL (initErrorOccurred is
set), stop initialization: break or return from the init function and avoid any
further use of transferBuffers (including skipping the oversized allocation for
transferBuffers[nbDmaBuffer + 1] and the subsequent putdefaultones loop). Ensure
putdefaultones is only called for buffers that were successfully allocated
(check transferBuffers[i] != NULL) and propagate/return the error state so init
doesn't crash when out-of-memory.
- Around line 1318-1333: The early-return branches (checking enableDriver,
initSuccess, leds) unconditionally call xSemaphoreGive(waitDisp) which creates
stale tokens; change them to only call xSemaphoreGive when a waiter is actually
pending on waitDisp (i.e. detect a pending waiter instead of always giving).
Update the code around waitDisp (used by waitDisplay()) to check the
semaphore/queue waiting count (e.g. use the FreeRTOS API such as
uxQueueMessagesWaiting/uxSemaphoreGetCount or the appropriate waiter-count
helper available in this codebase) and only call xSemaphoreGive(waitDisp) when
that check indicates a task is waiting.
🪄 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: 4017091d-225b-4501-b659-11d36b233a58
📒 Files selected for processing (7)
docs/developer/standardsandguidelines.mddocs/developer/workinprogress.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/colorarrangement.hsrc/esp32-d0s3_i2s_impl.hsrc/esp32-p4_parlio_impl.h
✅ Files skipped from review due to trivial changes (1)
- docs/developer/workinprogress.md
🚧 Files skipped from review as they are similar to previous changes (2)
- src/colorarrangement.h
- src/esp32-p4_parlio_impl.h
enhance deleteDriver() to properly reset the I2S and LCD_CAM peripherals
Now deleteDriver() properly resets the hardware:
- ESP32: Disables the I2S peripheral (ISR was already freed)
- ESP32-S3: Resets and disables the LCD_CAM peripheral (GDMA cleanup was already there)
- ESP32-P4: Already had full PARLIO cleanup
And updateDriver() now:
1. Calls full deleteDriver() to completely tear down all hardware
2. Reconfigures pins, geometry, and color order
3. Calls hwInit() to reinitialize the peripheral and ISR
4. Calls initTransferBuffers() to allocate and set up DMA buffers
This treats updateDriver() as a true reset-and-reconfigure operation, allowing runtime LED configuration changes on all platforms. The periph_module_disable() calls ensure clean shutdown, so periph_module_enable()
in hwInit() can properly reinitialize the hardware.
Now updateDriver() is much simpler and reuses the proven initled() initialization path:
1. Validate and wait for any in-flight transfer
2. Call deleteDriver() to completely tear down hardware and free buffers (peripherals are now properly reset/disabled)
3. Set nbDmaBuffer for the new configuration
4. Call initled(this->leds, pinsq, sizes, numStrips, channelsPerLight, pR, pG, pB, pW, pW2) which handles:
- Geometry setup (strip sizes, total LEDs)
- Color channel configuration
- Calls initLedImpl() which reinitializes hardware (setPins → hwInit → initTransferBuffers)
5. Restore brightness to the previous value
This eliminates code duplication, reuses the tested initled() path, and makes updateDriver() conceptually simpler: deleteDriver() + initled() = full reinitialization with new parameters.
Added proper error checking for all GDMA setup calls in S3's hwInit():
1. gdma_new_ahb_channel() / gdma_new_channel() — if allocation fails, set initErrorOccurred and return early
2. gdma_connect() — if connection fails, clean up the allocated channel with gdma_del_channel(), then set initErrorOccurred and return
3. gdma_apply_strategy() — if strategy fails, clean up the channel, set initErrorOccurred and return
4. gdma_register_tx_event_callbacks() — if callback registration fails, clean up the channel, set initErrorOccurred and return
Now if any GDMA call fails, initErrorOccurred is set to true, which makes initSuccess = false in initLedImpl(), preventing showPixels() from running with a broken/null GDMA channel. This prevents the watchdog hang
caused by a non-functional ISR.
if esp_intr_alloc() fails:
1. Error is logged with the failure reason
2. intrHandle is explicitly set to nullptr (defensive)
3. initErrorOccurred = true prevents initSuccess from being set to true in initLedImpl()
4. Early return prevents initTransferBuffers() from running and semaphores from being created
5. showPixels() / hwStart() won't run because initSuccess is false, preventing the attempt to enable a null interrupt
This mirrors the error handling just added for S3's GDMA calls.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 845-931: The PARLIO setup block must be entirely excluded when
HAS_PARLIO_DRIVER==0 to avoid references to undeclared symbols; wrap the whole
section that currently follows the "#if !HAS_PARLIO_DRIVER" early-return in an
`#if` HAS_PARLIO_DRIVER ... `#else` ... `#endif` so that when HAS_PARLIO_DRIVER is
false the file returns early (or contains a no-op) and when true the existing
code runs; specifically enclose all code that uses p4Config, p4TxUnit,
PARLIO_CLK_SRC_DEFAULT and calls parlio_new_tx_unit, parlio_tx_unit_enable,
parlio_del_tx_unit, parlio_tx_unit_wait_all_done, etc., inside the `#if`
HAS_PARLIO_DRIVER branch and put the current error log/return in the `#else`
branch.
🪄 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: 0165ee36-7c57-469b-b1df-2fc356dee4e8
📒 Files selected for processing (2)
src/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.h
🚧 Files skipped from review as they are similar to previous changes (1)
- src/I2SClocklessLedDriver.cpp
deleteDriver() (S3/ESP32): Removed the GDMA channel disconnect+delete (S3) and esp_intr_free (ESP32). These are one-time allocations tied to the LCD_CAM/I2S peripheral initialization which cannot be re-done. DMA buffer freeing is unchanged. updateDriver(): The hwInit() call is now P4-only inside if (wasInitialized). S3/ESP32 only reallocates DMA buffers — the GDMA channel and LCD_CAM config from initLedImpl() persist untouched. The wasInitialized guard remains to skip initTransferBuffers() when called before initled(). hwInit() (ESP32 path): Added idempotency guard for intrHandle — frees the existing ISR before registering a new one. This covers the edge case of deleteDriver() + initled() on ESP32, parallel to the existing S3 GDMA guard.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/I2SClocklessLedDriver.h (1)
812-819:⚠️ Potential issue | 🟠 MajorMissing null check after
allocateDMABuffer()calls.The loop allocates DMA buffers but does not verify each allocation succeeded. If
allocateDMABuffer()returnsNULLdue to OOM, Line 818 will dereference a null pointer causing a crash.🛡️ Proposed fix
for (int i = 0; i < nbDmaBuffer + 1; i++) { transferBuffers[i] = allocateDMABuffer(channelsPerLight * 8 * 2 * 3); + if (!transferBuffers[i]) { + ESP_LOGE(TAG, "initTransferBuffers: failed to allocate transferBuffers[%d]", i); + initErrorOccurred = true; + return; + } } transferBuffers[nbDmaBuffer + 1] = allocateDMABuffer(channelsPerLight * 8 * 2 * 3 * 4); + if (!transferBuffers[nbDmaBuffer + 1]) { + ESP_LOGE(TAG, "initTransferBuffers: failed to allocate transferBuffers[%d]", nbDmaBuffer + 1); + initErrorOccurred = true; + return; + } for (int i = 0; i < nbDmaBuffer; i++) { putdefaultones((uint16_t*)transferBuffers[i]->buffer);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 812 - 819, The allocation loop that calls allocateDMABuffer(...) to populate transferBuffers (and the subsequent special allocation for transferBuffers[nbDmaBuffer + 1]) must check each returned pointer for NULL to avoid dereferencing in putdefaultones; update the code around allocateDMABuffer, transferBuffers and putdefaultones to validate each allocation, handle failures by freeing any previously allocated buffers, set an error/return value or abort initialization (consistent with surrounding error handling), and avoid calling putdefaultones on a NULL buffer.
🧹 Nitpick comments (1)
src/I2SClocklessLedDriver.cpp (1)
62-109: Consider removing dead code path or documenting the workaround.The
#if true/#elsepattern at Line 62 leaves dead code in the#elsebranch. The comment mentions watchdog issues on ESP32-S3 but doesn't explain when the alternative path would be preferable.If this is a deliberate workaround, consider adding a named preprocessor macro (e.g.,
#define USE_DIRECT_REINIT 1) with a comment explaining when each path should be used. Otherwise, remove the dead#elsebranch to reduce confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.cpp` around lines 62 - 109, The `#if` true/#else block leaves a dead alternative reinit path unreferenced; either remove the `#else` branch or replace the magic true with a named macro and document the rationale. Replace the literal "#if true" with a descriptive macro like USE_DIRECT_REINIT (or delete the `#else` branch entirely), add a brief comment explaining when to use the alternate path (mentions of s3patch_inclhwInit, initled(...), and why setBrightness() was commented out), and ensure callers that rely on initTransferBuffers(), initSuccess, and initErrorOccurred keep the same behavior; update logging around initled/initTransferBuffers/initSuccess accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 812-819: The allocation loop that calls allocateDMABuffer(...) to
populate transferBuffers (and the subsequent special allocation for
transferBuffers[nbDmaBuffer + 1]) must check each returned pointer for NULL to
avoid dereferencing in putdefaultones; update the code around allocateDMABuffer,
transferBuffers and putdefaultones to validate each allocation, handle failures
by freeing any previously allocated buffers, set an error/return value or abort
initialization (consistent with surrounding error handling), and avoid calling
putdefaultones on a NULL buffer.
---
Nitpick comments:
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 62-109: The `#if` true/#else block leaves a dead alternative reinit
path unreferenced; either remove the `#else` branch or replace the magic true with
a named macro and document the rationale. Replace the literal "#if true" with a
descriptive macro like USE_DIRECT_REINIT (or delete the `#else` branch entirely),
add a brief comment explaining when to use the alternate path (mentions of
s3patch_inclhwInit, initled(...), and why setBrightness() was commented out),
and ensure callers that rely on initTransferBuffers(), initSuccess, and
initErrorOccurred keep the same behavior; update logging around
initled/initTransferBuffers/initSuccess accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dffc3bed-0866-4569-b0c4-eb1c78721a43
📒 Files selected for processing (2)
src/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.h
ajor refactoring of driver lifecycle functions (initled, updateDriver,
deleteDriver, deleteBuffers, initBuffers) to eliminate ~100 lines of
duplicate configuration code:
- Create applyConfiguration() helper to set all member variables
(geometry, color, pins, DMA buffer count, timing) in one place
- Merge initLedImpl() into canonical initled() — remove separate impl function
since it was only called from one place
- Merge initTransferBuffers() into initBuffers() for cleaner structure —
buffer allocation is now all in one place, easier to maintain
- Move sprite (HARDWARESPRITES) and hmap (__HARDWARE_MAP) allocation
into initBuffers() — now fully symmetric with deleteBuffers(), so
updateDriver(delete+reconfig) properly reallocates all buffers
- Fix platform conditional for FULL_DMA_BUFFER — now correctly nested
within #if CONFIG_IDF_TARGET_ESP32/S3 block (P4 had early return but
dependency was implicit)
- Remove s3patch_inclhwInit flag — no longer needed since updateDriver()
doesn't call initLedImpl() anymore
Result: cleaner architecture, no behavioral changes, easier to follow
reconfiguration path: deleteBuffers() → applyConfiguration() → initBuffers()
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/I2SClocklessLedDriver.cpp (1)
72-140: 🛠️ Refactor suggestion | 🟠 MajorAlign
.cppcontents with the repo’s header-first implementation rule.
deleteBuffers(),initBuffers(), andapplyConfiguration()are now implemented insrc/I2SClocklessLedDriver.cpp, but this repo’s rule forsrc/I2SClocklessLedDriver.{h,cpp}reserves.cppfor globals plusupdateDriver()/deleteDriver().As per coding guidelines
src/I2SClocklessLedDriver.{h,cpp}: Place nearly all implementation of the I2SClocklessLedDriver class in the header file src/I2SClocklessLedDriver.h, with only global variable definitions and updateDriver()/deleteDriver() in the .cpp.Also applies to: 207-401, 403-440
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.cpp` around lines 72 - 140, The implementations of deleteBuffers, initBuffers, and applyConfiguration in src/I2SClocklessLedDriver.cpp violate the repo rule that nearly all I2SClocklessLedDriver methods must live in the header; move these method bodies into src/I2SClocklessLedDriver.h (keeping their signatures in the class there) and leave src/I2SClocklessLedDriver.cpp only for global/static variable definitions and the required updateDriver()/deleteDriver() functions; ensure you remove the duplicated method definitions from the .cpp and update any includes or linkage as needed so the header contains the full implementations for deleteBuffers, initBuffers, and applyConfiguration while the .cpp contains only globals plus updateDriver()/deleteDriver().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 56-69: The driver fails to clear a previous failure flag and
restores brightness even when re-init fails; reset initErrorOccurred to false
before calling deleteBuffers()/applyConfiguration()/initBuffers(), then after
initBuffers() short-circuit on initErrorOccurred or failed init (compute
initSuccess using initErrorOccurred && numStrips>0 && numLedPerStrip>0) and only
call setBrightness(brightness) when initSuccess is true; update the logic around
initBuffers(), initSuccess, and setBrightness() to ensure a prior error doesn't
mask a new successful init and to avoid restoring brightness on failed re-init.
- Around line 274-287: The loop that fills dmaBuffersTransposed must check
allocateDMABuffer's return value before dereferencing; update the loop around
dmaBuffersTransposed and allocateDMABuffer to validate the returned pointer (for
both normal and FULL_DMA-sized allocations), on allocation failure stop the
loop, set an error/cleanup path that frees any previously allocated
dmaBuffersTransposed entries, and avoid accessing
dmaBuffersTransposed[i]->descriptor or dmaBuffersTransposed[i]->buffer (and
dmaBuffersTransposed[i-1]->descriptor.qe.stqe_next) when the current or previous
allocation is NULL; ensure putdefaultones is only called when
dmaBuffersTransposed[i] is non-NULL and set descriptor.eof and stqe_next only on
valid buffers.
---
Outside diff comments:
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 72-140: The implementations of deleteBuffers, initBuffers, and
applyConfiguration in src/I2SClocklessLedDriver.cpp violate the repo rule that
nearly all I2SClocklessLedDriver methods must live in the header; move these
method bodies into src/I2SClocklessLedDriver.h (keeping their signatures in the
class there) and leave src/I2SClocklessLedDriver.cpp only for global/static
variable definitions and the required updateDriver()/deleteDriver() functions;
ensure you remove the duplicated method definitions from the .cpp and update any
includes or linkage as needed so the header contains the full implementations
for deleteBuffers, initBuffers, and applyConfiguration while the .cpp contains
only globals plus updateDriver()/deleteDriver().
🪄 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: d8bf8019-1e12-4b82-8af0-13ec7f260527
📒 Files selected for processing (2)
src/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.h
🚧 Files skipped from review as they are similar to previous changes (1)
- src/I2SClocklessLedDriver.h
|
@coderabbitai, regarding
This doesn't make sense as normal practice is to place definitions in .h and implementations in .cpp. we follow that rule, please update your learnings |
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
⛔ Learnings removed🧠 Learnings used |
The current updateDriver() code correctly handles all five reconfiguration scenarios: - Strip count changes: numStrips, stripSize[], firstIndexPerOutput[] properly updated; stale entries beyond numStrips are never accessed - LED count changes: numLedPerStrip and per-strip poli advancement work correctly for variable lengths - Pin changes: memset(pins, 0, MAX_PINS) clears stale entries; setPins() → esp_rom_gpio_connect_out_signal correctly re-routes GPIO mux - Color order changes: all offset fields updated; setBrightness() correctly allocates/frees whiteMap/white2Map - DMA buffer count changes: deleteBuffers() is called BEFORE applyConfiguration(), so it uses the old nbDmaBuffer to free the right number — this ordering is critical and correct The "random colors" root cause remains unidentified through static analysis. The remaining candidates are LCD_CAM register state that periph_module_reset() (in hwInit()) would clear but hwStart() doesn't, or a cache coherency issue with the PSRAM-allocated pointer array.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/I2SClocklessLedDriver.h (1)
476-558:⚠️ Potential issue | 🟡 MinorPotential memory leak on retry after partial
setBrightness()failure.If
setBrightness()fails mid-way (e.g.,greenMapallocation fails afterredMapsucceeds),initErrorOccurredis set but the successfully allocated maps are not freed. Wheninitled()is retried, line 1367 resetsinitErrorOccurredbut the orphaned allocations remain.The existing null-checks (
if (!redMap)) prevent double-allocation, so this is not a crash risk—but after a failed init attempt, the partially allocated LUTs will persist untildeleteDriver()or destructor cleanup.🛡️ Suggested defensive cleanup at retry
Consider adding a helper or extending the error path to free any partially allocated LUTs before returning, so a retry starts with a clean slate:
// In setBrightness(), after any allocation failure: if (!redMap || !greenMap || !blueMap) { free(redMap); redMap = nullptr; free(greenMap); greenMap = nullptr; free(blueMap); blueMap = nullptr; // ... and whiteMap/white2Map if applicable }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 476 - 558, The setBrightness function can leave partially allocated LUTs on allocation failure; modify setBrightness to free any already-allocated maps (redMap, greenMap, blueMap, whiteMap, white2Map) and set them to nullptr before returning on error (where initErrorOccurred/initSuccess are set), so retries (e.g., in initled) start with a clean slate; ensure the same cleanup logic is applied for each early-return allocation failure path and reuse the same helper cleanup routine if present (or add one) and ensure deleteDriver/destructor still handle final cleanup.
♻️ Duplicate comments (1)
src/esp32-d0s3_i2s_impl.h (1)
239-247:⚠️ Potential issue | 🟠 MajorDuplicate semaphore release after
hwStop()on ESP32.
hwStop(driver)at line 241 already releasesdriver->semwhendriver->isWaitingis true (see lines 194-196). The subsequent block at lines 242-246 releasessemagain under the same condition, causing a double semaphore give. This can corrupt the semaphore count, potentially allowing an extra take on the next frame.🐛 Proposed fix: Remove duplicate release
if (GET_PERI_REG_BITS(I2S_INT_ST_REG(I2S_DEVICE), I2S_OUT_TOTAL_EOF_INT_ST_S, I2S_OUT_TOTAL_EOF_INT_ST_S)) { hwStop(driver); - if (driver->isWaiting) { - portBASE_TYPE hpTaskAwoken = 0; - xSemaphoreGiveFromISR(driver->sem, &hpTaskAwoken); - if (hpTaskAwoken == pdTRUE) portYIELD_FROM_ISR(); - } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/esp32-d0s3_i2s_impl.h` around lines 239 - 247, The ISR currently calls hwStop(driver), which already gives driver->sem when driver->isWaiting is true; remove the subsequent duplicate semaphore release block (the xSemaphoreGiveFromISR(...) guarded by driver->isWaiting and portYIELD_FROM_ISR()) that immediately follows hwStop in the I2S EOF handler to avoid double-giving the semaphore. Keep the EOF check and hwStop(driver) call intact; ensure only hwStop performs the semaphore give for driver->isWaiting so symbols to edit are the EOF interrupt block, hwStop, driver->isWaiting and driver->sem usage.
🧹 Nitpick comments (1)
docs/developer/workinprogress.md (1)
474-485: Add language specifier to fenced code block.The code block at line 474 is missing a language specifier, which triggers a markdownlint warning (MD040). Since this shows a text-based sequence diagram, use
textorplaintext.📝 Proposed fix
-``` +```text validate args if isDisplaying: wait for in-flight DMA via waitDisp semaphore (released by ISR in hwStop)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/workinprogress.md` around lines 474 - 485, The fenced code block containing the sequence diagram (lines showing "validate args", "isDisplaying", "waitDisp", "deleteBuffers()", "applyConfiguration()", "initBuffers()", "setBrightness()") is missing a language specifier and triggers markdownlint MD040; update the triple-backtick fence to include a language such as text or plaintext (e.g., ```text) so the block is properly marked, leaving the contents (validate args, waitDisp semaphore note, deleteBuffers(), applyConfiguration(), initBuffers(), setPins(), setShowDelay(), setBrightness()) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 1264-1272: When loadAndTranspose() returns false on the ESP32P4
path, the code sets isDisplaying = false but does not release the waitDisp
semaphore, which can deadlock showPixels() waiting on waitDisp
(wasWaitingtofinish). Fix by signaling/releasing waitDisp in the failure branch
(using the same semaphore release API used elsewhere in this module) immediately
after detecting loadAndTranspose() == false, and ensure isDisplaying is set to
false; do not call hwStart()/hwStop() in that failure case. Reference symbols:
loadAndTranspose, isDisplaying, waitDisp, showPixels, wasWaitingtofinish,
NO_WAIT, hwStart/hwStop.
---
Outside diff comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 476-558: The setBrightness function can leave partially allocated
LUTs on allocation failure; modify setBrightness to free any already-allocated
maps (redMap, greenMap, blueMap, whiteMap, white2Map) and set them to nullptr
before returning on error (where initErrorOccurred/initSuccess are set), so
retries (e.g., in initled) start with a clean slate; ensure the same cleanup
logic is applied for each early-return allocation failure path and reuse the
same helper cleanup routine if present (or add one) and ensure
deleteDriver/destructor still handle final cleanup.
---
Duplicate comments:
In `@src/esp32-d0s3_i2s_impl.h`:
- Around line 239-247: The ISR currently calls hwStop(driver), which already
gives driver->sem when driver->isWaiting is true; remove the subsequent
duplicate semaphore release block (the xSemaphoreGiveFromISR(...) guarded by
driver->isWaiting and portYIELD_FROM_ISR()) that immediately follows hwStop in
the I2S EOF handler to avoid double-giving the semaphore. Keep the EOF check and
hwStop(driver) call intact; ensure only hwStop performs the semaphore give for
driver->isWaiting so symbols to edit are the EOF interrupt block, hwStop,
driver->isWaiting and driver->sem usage.
---
Nitpick comments:
In `@docs/developer/workinprogress.md`:
- Around line 474-485: The fenced code block containing the sequence diagram
(lines showing "validate args", "isDisplaying", "waitDisp", "deleteBuffers()",
"applyConfiguration()", "initBuffers()", "setBrightness()") is missing a
language specifier and triggers markdownlint MD040; update the triple-backtick
fence to include a language such as text or plaintext (e.g., ```text) so the
block is properly marked, leaving the contents (validate args, waitDisp
semaphore note, deleteBuffers(), applyConfiguration(), initBuffers(), setPins(),
setShowDelay(), setBrightness()) unchanged.
🪄 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: f1b94ccf-8ffa-4c32-9126-fa87206cfa2a
📒 Files selected for processing (6)
docs/developer/developer.mddocs/developer/workinprogress.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/colorarrangement.hsrc/esp32-d0s3_i2s_impl.h
✅ Files skipped from review due to trivial changes (1)
- src/colorarrangement.h
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/developer/developer.md
- src/I2SClocklessLedDriver.cpp
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/I2SClocklessLedDriver.h (1)
948-955:⚠️ Potential issue | 🔴 CriticalFix the undefined
Widentifier in the FULL_DMA white-extraction helpers.Both overloads use
Winstead of the localwhitevariable, so this path does not compile whenFULL_DMA_BUFFERis enabled.🐛 Proposed fix
if (offsetWhite != UINT8_MAX) { white = MIN(red, green); - white = MIN(W, blue); + white = MIN(white, blue); red = red - white; green = green - white; blue = blue - white; } - setPixelinBuffer(pos, red, green, blue, W); + setPixelinBuffer(pos, red, green, blue, white);Also applies to: 1052-1060
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 948 - 955, The FULL_DMA white-extraction helpers erroneously use an undefined identifier `W`; replace those uses with the local `white` variable (e.g. change `white = MIN(W, blue);` to `white = MIN(white, blue);`) in both overloads so the MIN calculations and subsequent subtractions (red/green/blue = red/green/blue - white) compile correctly and still call setPixelinBufferByStrip(stripNumber, posOnStrip, red, green, blue, white).
♻️ Duplicate comments (2)
src/esp32-d0s3_i2s_impl.h (1)
239-246:⚠️ Potential issue | 🟡 MinorRemove the second
semgive afterhwStop(driver).
hwStop()already releasesdriver->semwhendriver->isWaitingis true, so this extra block is redundant and can leave the WAIT path observing a spurious extra signal. This was already reported on the older inline implementation and looks like it came across during the extraction.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/esp32-d0s3_i2s_impl.h` around lines 239 - 246, The code is duplicating semaphore release: after calling hwStop(driver) the block that checks driver->isWaiting and calls xSemaphoreGiveFromISR(driver->sem, &hpTaskAwoken) (and portYIELD_FROM_ISR()) must be removed because hwStop() already gives driver->sem when driver->isWaiting is true; locate the I2S interrupt handler section around I2S_OUT_TOTAL_EOF_INT_ST handling and delete the redundant isWaiting/xSemaphoreGiveFromISR/portYIELD_FROM_ISR block so only hwStop(driver) performs the semaphore give.src/I2SClocklessLedDriver.h (1)
1265-1271:⚠️ Potential issue | 🟠 MajorRelease
waitDispwhen the P4 frame is skipped.If
loadAndTranspose()fails here,hwStop()never runs, so the normal waiter-release path is skipped too. Any concurrent caller already blocked inwaitDisplay()can stay stuck even thoughisDisplayingis cleared. This is the same deadlock shape that was flagged earlier on this path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 1265 - 1271, The code currently skips hwStop() and the waiter-release when loadAndTranspose() fails, leaving callers blocked in waitDisplay(); modify the control flow in the display sequence so that whenever isDisplaying is set/cleared around loadAndTranspose() you always perform the waiter release path (either call hwStop() and the existing waitDisp release logic or explicitly signal/release waitDisp) even on failure; update the block around loadAndTranspose(), hwStart(), hwStop(), isDisplaying and the waiter (waitDisp / waitDisplay()) so the error branch calls the same cleanup/release routine used on success.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/developer/workinprogress.md`:
- Around line 158-163: The lifecycle table is outdated for ESP32-P4: update the
P4 row so hwInit() is marked as a no-op/returns immediately on P4 (it no longer
configures PARLIO), and move the PARLIO configuration and real buffer allocation
entries to initBuffers() (which now does the real PARLIO setup and PSRAM
ping-pong buffer work), while leaving initTransferBuffers() and other phases
descriptions aligned with the new flow used by initBuffers(),
loadAndTranspose(), hwStart(), and hwStop(); also update the “completed phases”
notes to reflect that PARLIO setup and buffer allocation for P4 are completed in
initBuffers() rather than hwInit().
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 238-252: The hmap buffer is underallocated (malloc uses totalLeds
* 2 bytes) but hmap is uint32_t* and createhardwareMap() writes 32-bit entries;
change the allocation in the initBuffers block that sets hmap to allocate
totalLeds * sizeof(uint32_t) (or totalLeds * sizeof(*hmap)) instead of totalLeds
* 2 so the buffer can hold one uint32_t per LED; keep the existing null checks
and error handling around hmap and then call createhardwareMap() as before.
- Around line 296-324: The loop wires DMA buffer descriptors using only the
ESP32 descriptor fields (dmaBuffersTransposed[i]->descriptor.*), which breaks
the ESP32-S3 path that uses dw0/next in I2SClocklessLedDriverDMABuffer; update
the wiring to branch by target/descriptor layout and set the correct fields:
when using the ESP32 descriptor use descriptor.qe.stqe_next and descriptor.eof
as currently done, and when targeting ESP32-S3 use the dw0.next (or appropriate
next field) and the S3-specific EOF/flags field on
I2SClocklessLedDriverDMABuffer; similarly call putdefaultones only on the path
that requires it. Implement this via an `#if/`#ifdef (or a small helper function)
that checks the platform macro (e.g., CONFIG_IDF_TARGET_ESP32 vs
CONFIG_IDF_TARGET_ESP32S3) or inspects the struct, and update the loop wiring
for dmaBuffersTransposed, descriptor, dw0, next, and eof accordingly so both
platforms build and the DMA chain is linked correctly.
In `@src/I2SClocklessLedDriver.h`:
- Around line 1360-1379: The initled public overload must validate canonical
channel-layout args before saving them or calling applyConfiguration: in
initled(uint8_t* leds, uint8_t* pinsq, uint16_t* sizes, uint8_t numStrips,
uint8_t channelsPerLight, uint8_t offsetRed, uint8_t offsetGreen, uint8_t
offsetBlue, uint8_t offsetWhite = UINT8_MAX, uint8_t offsetWhite2 = UINT8_MAX,
bool extractWhiteFromRGB = false) validate that channelsPerLight is between 3
and 5, that offsetRed/offsetGreen/offsetBlue are each < channelsPerLight, that
offsetWhite and offsetWhite2 are either UINT8_MAX or < channelsPerLight, and
that no two present offsets (excluding UINT8_MAX) are duplicates; if any check
fails log an error via ESP_LOGE (include values) set initErrorOccurred = true
and return without changing initSuccess/leds/saveleds or calling
applyConfiguration; keep use of ColorArrangement/applyColorArrangement and
rgbwBufferMapping intact but ensure these illegal states are rejected here.
- Around line 1191-1206: The code currently calls xSemaphoreGive(waitDisp) in
error branches unconditionally, which can leave a stale token if no waiter is
registered; modify the three branches (the early-return blocks checking
enableDriver, initSuccess, and leds == NULL) to only give the semaphore when a
consumer is actually waiting by checking the waiter flag (wasWaitingtofinish or
similar) before calling xSemaphoreGive(waitDisp), and reset the flag
appropriately so waitDisplay() will not consume a stale token; locate these
checks near the isDisplaying handling in I2SClocklessLedDriver (look for
waitDisp, wasWaitingtofinish, waitDisplay) and implement the conditional
semaphore give and flag clear there.
---
Outside diff comments:
In `@src/I2SClocklessLedDriver.h`:
- Around line 948-955: The FULL_DMA white-extraction helpers erroneously use an
undefined identifier `W`; replace those uses with the local `white` variable
(e.g. change `white = MIN(W, blue);` to `white = MIN(white, blue);`) in both
overloads so the MIN calculations and subsequent subtractions (red/green/blue =
red/green/blue - white) compile correctly and still call
setPixelinBufferByStrip(stripNumber, posOnStrip, red, green, blue, white).
---
Duplicate comments:
In `@src/esp32-d0s3_i2s_impl.h`:
- Around line 239-246: The code is duplicating semaphore release: after calling
hwStop(driver) the block that checks driver->isWaiting and calls
xSemaphoreGiveFromISR(driver->sem, &hpTaskAwoken) (and portYIELD_FROM_ISR())
must be removed because hwStop() already gives driver->sem when
driver->isWaiting is true; locate the I2S interrupt handler section around
I2S_OUT_TOTAL_EOF_INT_ST handling and delete the redundant
isWaiting/xSemaphoreGiveFromISR/portYIELD_FROM_ISR block so only hwStop(driver)
performs the semaphore give.
In `@src/I2SClocklessLedDriver.h`:
- Around line 1265-1271: The code currently skips hwStop() and the
waiter-release when loadAndTranspose() fails, leaving callers blocked in
waitDisplay(); modify the control flow in the display sequence so that whenever
isDisplaying is set/cleared around loadAndTranspose() you always perform the
waiter release path (either call hwStop() and the existing waitDisp release
logic or explicitly signal/release waitDisp) even on failure; update the block
around loadAndTranspose(), hwStart(), hwStop(), isDisplaying and the waiter
(waitDisp / waitDisplay()) so the error branch calls the same cleanup/release
routine used on success.
🪄 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: b41c5956-cc0b-421f-b8b6-7d7acaad8b40
📒 Files selected for processing (6)
docs/developer/developer.mddocs/developer/workinprogress.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/colorarrangement.hsrc/esp32-d0s3_i2s_impl.h
✅ Files skipped from review due to trivial changes (2)
- src/colorarrangement.h
- docs/developer/developer.md
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
docs/developer/standardsandguidelines.md (2)
163-174: Consider platform-specific ISR-path guidance.The ISR-path review guidance is accurate for ESP32/S3 (where
loadAndTransposeis called from the DMA ISR), but on ESP32-P4,loadAndTransposeis called synchronously from task context and does not have ISR-safety concerns.Consider adding a note distinguishing the platforms:
- ESP32/S3:
loadAndTranspose,hwStop,interruptHandlerare ISR-context → require IRAM placement and ISR-safe APIs- ESP32-P4: PARLIO path operates in task context → standard task-level constraints apply
Based on learnings, "on ESP32-P4,
loadAndTransposeis called synchronously fromshowPixelsImplin task context, not from any ISR or DMA interrupt handler."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/standardsandguidelines.md` around lines 163 - 174, Update the ISR-path guidance to distinguish platform behaviors: state that for ESP32 and S3 the functions loadAndTranspose, hwStop, and interruptHandler are called from DMA ISR context and therefore must be IRAM-placed and use only ISR-safe APIs, while for ESP32-P4 note that loadAndTranspose is invoked synchronously from showPixelsImpl in task context (PARLIO path) so it does not require ISR-safety or IRAM placement and standard task-level constraints apply; mention these specific symbols (loadAndTranspose, hwStop, interruptHandler, showPixelsImpl) so reviewers know where to apply platform-specific rules.
274-288: Debugging prompt assumes ISR constraints.The example debugging prompt includes "the fix must not affect the ISR hot path performance," which is ESP32/S3-specific. On ESP32-P4, the PARLIO path operates in task context without ISR hot-path concerns.
Consider either:
- Noting this constraint applies to ESP32/S3 paths
- Providing a separate P4-specific debugging prompt pattern
Based on learnings, ESP32-P4 PARLIO operates synchronously from task context.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/standardsandguidelines.md` around lines 274 - 288, Update the "Debugging a crash / hard fault" prompt example to clarify the ISR constraint: in the prompt text (the "The ESP32 crashes..." block under the "Debugging a crash / hard fault" header) add a note that the constraint "the fix must not affect the ISR hot path performance" applies specifically to ESP32/S3 PARLIO/ISR hot-paths, and add an alternative P4-specific variant that states "for ESP32-P4 PARLIO operates in task context; fixes may assume no ISR hot-path performance constraint" so reviewers know to use the S3 constraint only for S3 targets and the P4 variant for ESP32-P4.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 359-446: initBuffers() currently creates/enables the PARLIO TX
unit eagerly (parlio_new_tx_unit / parlio_tx_unit_enable / parlio_del_tx_unit /
parlio_tx_unit_wait_all_done) — move that hardware setup out of initBuffers and
defer it to a lazy initializer invoked from showPixels(). Remove any calls that
create/enable/disable p4TxUnit from initBuffers (leave size checks, p4Config
population and set p4TxUnit = NULL), and implement an
ensureParlioTxUnitInitialized() (or similar) called at start of showPixels()
that performs parlio_new_tx_unit, parlio_tx_unit_enable, and the existing error
logging/initErrorOccurred handling; keep cleanup of p4TxUnit only in driver
shutdown or explicit deinit where appropriate. Ensure showPixels() uses
ensureParlioTxUnitInitialized() before submitting transfers so reconfiguration
remains buffer-only and hardware is brought up on first use.
- Around line 33-37: The updateDriver call currently only validates
pins/sizes/numStrips/dmaBuffer but not channels or offsets, which can cause
out-of-bounds access; inside I2SClocklessLedDriver::updateDriver validate that
channelsPerLight is within the allowed range (1..5) and that all offset
parameters (offsetRed, offsetGreen, offsetBlue, offsetWhite, offsetWhite2) are
within [0, channelsPerLight-1] or marked unused, rejecting the call (log
ESP_LOGE and return) on any invalid combination; make the same checks where
updateDriver is referenced/used (the transpose/mapping path) so the fixed-size
channel buffers never receive an invalid channel index.
- Around line 266-279: When an allocation in initBuffers fails, currently
previously allocated transferBuffers[] entries are not freed; modify initBuffers
to on any allocation failure iterate over already-allocated
transferBuffers[0..i-1] and free them (call the matching free function, e.g.,
freeDMABuffer) and set those pointers to nullptr, then set initErrorOccurred and
return; do the same when allocating transferBuffers[nbDmaBuffer + 1] (free
transferBuffers[0..nbDmaBuffer] if the final allocation fails) so no partial
allocations remain.
---
Nitpick comments:
In `@docs/developer/standardsandguidelines.md`:
- Around line 163-174: Update the ISR-path guidance to distinguish platform
behaviors: state that for ESP32 and S3 the functions loadAndTranspose, hwStop,
and interruptHandler are called from DMA ISR context and therefore must be
IRAM-placed and use only ISR-safe APIs, while for ESP32-P4 note that
loadAndTranspose is invoked synchronously from showPixelsImpl in task context
(PARLIO path) so it does not require ISR-safety or IRAM placement and standard
task-level constraints apply; mention these specific symbols (loadAndTranspose,
hwStop, interruptHandler, showPixelsImpl) so reviewers know where to apply
platform-specific rules.
- Around line 274-288: Update the "Debugging a crash / hard fault" prompt
example to clarify the ISR constraint: in the prompt text (the "The ESP32
crashes..." block under the "Debugging a crash / hard fault" header) add a note
that the constraint "the fix must not affect the ISR hot path performance"
applies specifically to ESP32/S3 PARLIO/ISR hot-paths, and add an alternative
P4-specific variant that states "for ESP32-P4 PARLIO operates in task context;
fixes may assume no ISR hot-path performance constraint" so reviewers know to
use the S3 constraint only for S3 targets and the P4 variant for ESP32-P4.
🪄 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: aad3657e-7395-4eb8-b498-118195facaa8
📒 Files selected for processing (7)
docs/developer/developer.mddocs/developer/standardsandguidelines.mddocs/developer/workinprogress.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/colorarrangement.hsrc/esp32-d0s3_i2s_impl.h
✅ Files skipped from review due to trivial changes (1)
- src/colorarrangement.h
🚧 Files skipped from review as they are similar to previous changes (3)
- docs/developer/workinprogress.md
- src/I2SClocklessLedDriver.h
- src/esp32-d0s3_i2s_impl.h
| void I2SClocklessLedDriver::updateDriver(uint8_t* pinsq, uint16_t* sizes, uint8_t numStrips, uint8_t dmaBuffer, uint8_t channelsPerLight, uint8_t offsetRed, uint8_t offsetGreen, uint8_t offsetBlue, uint8_t offsetWhite, uint8_t offsetWhite2, bool extractWhiteFromRGB) { | ||
| if (pinsq == nullptr || sizes == nullptr || numStrips == 0 || numStrips > MAX_PINS || dmaBuffer == 0) { | ||
| ESP_LOGE(TAG, "updateDriver: invalid args numStrips=%u dmaBuffer=%u sizes=%p pinsq=%p", numStrips, dmaBuffer, (void*)sizes, (void*)pinsq); | ||
| return; | ||
| return; // leave driver in previous consistent state | ||
| } |
There was a problem hiding this comment.
Validate channelsPerLight and channel offsets before storing them.
Invalid channelsPerLight / offset combinations can lead to out-of-bounds access in the transpose/mapping path (fixed-size channel buffers expect max 5 channels). Please reject invalid values early in updateDriver().
🛠️ Proposed guardrails
void I2SClocklessLedDriver::updateDriver(uint8_t* pinsq, uint16_t* sizes, uint8_t numStrips, uint8_t dmaBuffer, uint8_t channelsPerLight, uint8_t offsetRed, uint8_t offsetGreen, uint8_t offsetBlue, uint8_t offsetWhite, uint8_t offsetWhite2, bool extractWhiteFromRGB) {
if (pinsq == nullptr || sizes == nullptr || numStrips == 0 || numStrips > MAX_PINS || dmaBuffer == 0) {
ESP_LOGE(TAG, "updateDriver: invalid args numStrips=%u dmaBuffer=%u sizes=%p pinsq=%p", numStrips, dmaBuffer, (void*)sizes, (void*)pinsq);
return; // leave driver in previous consistent state
}
+ if (channelsPerLight < 3 || channelsPerLight > 5) {
+ ESP_LOGE(TAG, "updateDriver: invalid channelsPerLight=%u (expected 3..5)", channelsPerLight);
+ return;
+ }
+ auto isOptionalOffsetValid = [channelsPerLight](uint8_t o) {
+ return o == UINT8_MAX || o < channelsPerLight;
+ };
+ if (offsetRed >= channelsPerLight || offsetGreen >= channelsPerLight || offsetBlue >= channelsPerLight ||
+ !isOptionalOffsetValid(offsetWhite) || !isOptionalOffsetValid(offsetWhite2)) {
+ ESP_LOGE(TAG, "updateDriver: invalid channel offsets for channelsPerLight=%u", channelsPerLight);
+ return;
+ }Also applies to: 454-477
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/I2SClocklessLedDriver.cpp` around lines 33 - 37, The updateDriver call
currently only validates pins/sizes/numStrips/dmaBuffer but not channels or
offsets, which can cause out-of-bounds access; inside
I2SClocklessLedDriver::updateDriver validate that channelsPerLight is within the
allowed range (1..5) and that all offset parameters (offsetRed, offsetGreen,
offsetBlue, offsetWhite, offsetWhite2) are within [0, channelsPerLight-1] or
marked unused, rejecting the call (log ESP_LOGE and return) on any invalid
combination; make the same checks where updateDriver is referenced/used (the
transpose/mapping path) so the fixed-size channel buffers never receive an
invalid channel index.
There was a problem hiding this comment.
@coderabbitai, Don't want to do that as in the future I want to support lights with more than 5 channels, what exactly will happen if I do that?
There was a problem hiding this comment.
🧩 Analysis chain
Let me look at the actual transpose/mapping paths to give you a precise answer.
🏁 Script executed:
#!/bin/bash
# Find fixed-size arrays related to channels in the impl headers and main header
rg -n "secondPixel|channelsPerLight|channel|NBIS2SERIALPINS|MAX_STRIPS|5\]|4\]|3\]" \
src/esp32-d0s3_i2s_impl.h src/esp32-p4_parlio_impl.h src/I2SClocklessLedDriver.h \
--no-heading -A1 -B1 2>/dev/null | head -120
echo "---SECONDPIXEL DETAIL---"
rg -n "secondPixel" src/esp32-d0s3_i2s_impl.h -A3 -B3 2>/dev/null
echo "---CHANNEL LOOP in loadAndTranspose---"
rg -n "channelsPerLight|for.*channel\|for.*comp\|for.*i.*channel" src/esp32-d0s3_i2s_impl.h -A2 -B1 2>/dev/null
echo "---P4 loadAndTranspose channel loop---"
rg -n "channelsPerLight|for.*channel\|for.*comp\|for.*i.*channel" src/esp32-p4_parlio_impl.h -A2 -B1 2>/dev/null
echo "---HARDCODED SIZES in header---"
rg -n "uint.*\[5\]|uint.*\[4\]|uint.*\[3\]|MAX_CHANNELS\|MAXCHANNELS" src/I2SClocklessLedDriver.h 2>/dev/null
Length of output: 15278
@ewowi, good to know — here is exactly what happens today on each path when channelsPerLight > 5 is passed:
ESP32 / ESP32-S3 path (esp32-d0s3_i2s_impl.h)
1. Stack buffer overflow (undefined behaviour / likely crash)
loadAndTranspose allocates a fixed-size temp array:
uint8_t mapped[5] = {}; // line 417 — always 5 elements
driver->rgbwBufferMapping(poli, mapped);rgbwBufferMapping writes to dst[offsetRed], dst[offsetGreen], dst[offsetBlue], dst[offsetWhite], dst[offsetWhite2]. If any of those offsets is ≥ 5 (which it will be with 6+ channels), the write goes beyond the end of the mapped array → stack corruption / crash.
To support N channels this array needs to be uint8_t mapped[channelsPerLight] (a VLA, like secondPixel already is).
2. Silent data loss for channels beyond index 4
The transposeColorChannel calls are hardcoded to exactly 5 indices:
transposeColorChannel(secondPixel[0].bytes, buffer + 0 * 3 * 8, ...);
transposeColorChannel(secondPixel[1].bytes, buffer + 1 * 3 * 8, ...);
transposeColorChannel(secondPixel[2].bytes, buffer + 2 * 3 * 8, ...);
if (offsetWhite != UINT8_MAX) transposeColorChannel(secondPixel[3].bytes, buffer + 3 * 3 * 8, ...);
if (offsetWhite2 != UINT8_MAX) transposeColorChannel(secondPixel[4].bytes, buffer + 4 * 3 * 8, ...);A 6th (or 7th…) channel would be populated in secondPixel but never transposed into the DMA buffer — silently dropped output, no crash.
3. rgbwBufferMapping hardcodes src[3] / src[4]
uint8_t white = src[3];
if (offsetWhite2 != UINT8_MAX) dst[offsetWhite2] = white2Map[src[4]];For a hypothetical 6-channel LED (e.g. RGBWUV) the UV bytes at src[5] / src[6] are simply never read.
ESP32-P4 path (esp32-p4_parlio_impl.h)
The P4 path is already mostly future-proof — it uses a VLA for the mapped buffer and a dynamic for loop:
uint8_t mappedBuffer[channelsPerLight * SOC_PARLIO_TX_UNIT_MAX_DATA_WIDTH]; // VLA
for (uint32_t component_in_pixel = 0; component_in_pixel < channelsPerLight; ++component_in_pixel) { ... }The only shared bottleneck is rgbwBufferMapping (point 3 above).
Summary table
| Location | channelsPerLight > 5 effect |
|---|---|
mapped[5] in loadAndTranspose (ESP32/S3) |
Stack buffer overflow / crash |
Hardcoded transposeColorChannel calls (ESP32/S3) |
Channels 5+ silently not output |
rgbwBufferMapping src[3]/src[4] (all targets) |
Channels 5+ never read from pixel buffer |
P4 loadAndTranspose buffer + loop |
✅ Already dynamic — safe |
So the validation I suggested would hide these bugs rather than fix them. The right path forward (when you're ready) is to change mapped[5] → mapped[channelsPerLight], replace the five hardcoded transposeColorChannel calls with a loop, and generalise rgbwBufferMapping's src indexing. I'll withdraw the guard suggestion.
✏️ Learnings added
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 54
File: src/I2SClocklessLedDriver.cpp:33-37
Timestamp: 2026-04-11T16:00:12.799Z
Learning: In hpwit/I2SClocklessLedDriver (src/esp32-d0s3_i2s_impl.h), `loadAndTranspose` uses a hardcoded `uint8_t mapped[5]` temporary array and five hardcoded `transposeColorChannel` calls (indices 0-4). Supporting channelsPerLight > 5 requires: (1) changing `mapped[5]` to a VLA `mapped[channelsPerLight]`, (2) replacing the five hardcoded `transposeColorChannel` calls with a loop over `channelsPerLight`, and (3) generalising `rgbwBufferMapping` in `I2SClocklessLedDriver.h` which hardcodes `src[3]` for W and `src[4]` for W2. The ESP32-P4 path in `esp32-p4_parlio_impl.h` already uses VLAs and a dynamic loop so it is largely future-proof.
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: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/I2SClocklessLedDriver.{h,cpp} : Implement two DMA pipeline modes: **Ping-pong DMA** (default) with two small buffers filled incrementally by ISR (low RAM, CPU on each ISR call), and **Full DMA buffer** (enabled with `#define FULL_DMA_BUFFER`) that pre-transposes the entire frame for autonomous I2S operation. Full DMA buffer enables `showPixelsFirstTranspose()`, `showPixelsFromBuffer()`, and `showPixelsFromBuffer(LOOP)` methods.
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-05T18:21:16.152Z
Learning: In the I2SClocklessLedDriver library (hpwit/I2SClocklessLedDriver), the user (ewowi) confirmed that the following are correctly implemented as of PR `#51` / commit 450d5c1: getStrip() bounds guard, getLengths() uint16_t* return type, createSubset clamping, frameBuffer null-deref protection with _offPixel sentinel and valid(), hardwareSprite NBSPRITE overflow guard, deleteDriver() nulling DMABuffersTampon, freeing target and _hmap, ISR semaphore logic, and main.cpp effect cycling. The namespace wrapper for struct Pixel / class Pixels / enum class leddirection in src/___pixeltypes.h is intentionally deferred as a missing feature (not a bug).
Learnt from: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/I2SClocklessLedDriver.{h,cpp} : Use FreeRTOS semaphores on the driver object with specific purposes: `sem` blocks `showPixels(WAIT)` until transfer completes, `semSync` signals frame-sync for `waitSync()`, and `waitDisp` is lazy-created in `updateDriver()` to wait for in-flight DMA before reconfiguration. `waitDisp` must be released from ISR via `xSemaphoreGiveFromISR`.
Learnt from: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/I2SClocklessLedDriver.{h,cpp} : Define compile-time options (set before `#include "I2SClocklessLedDriver.h"`) for: `FULL_DMA_BUFFER`, `ENABLE_HARDWARE_SCROLL`, `USE_PIXELSLIB`, `HARDWARESPRITES 1`, `SNAKEPATTERN`, `ALTERNATEPATTERN`, `OVERCLOCK_1MHZ`, `OVERCLOCK_1_1MHZ`, `OVER_CLOCK_MAX`. These control optional features and hardware configurations.
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-10T12:35:37.089Z
Learning: In hpwit/I2SClocklessLedDriver, the project follows standard C++ practice: class declarations, type definitions, and inline helpers go in `.h` header files, while function/method implementations go in `.cpp` source files. This applies to `src/I2SClocklessLedDriver.h` (class declaration, inline helpers) and `src/I2SClocklessLedDriver.cpp` (method implementations such as `updateDriver()`, `deleteDriver()`, `initBuffers()`, `applyConfiguration()`). Do NOT flag `.cpp` implementations as violating a "header-first" rule — that rule does not apply here.
Learnt from: MoonModules
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-08T10:40:10.745Z
Learning: In hpwit/I2SClocklessLedDriver (src/parlio_p4.cpp), `transpose_32_slices` is declared `inline` and must NOT be given `IRAM_ATTR`: (1) `inline` and `IRAM_ATTR` are incompatible — an inline function has no out-of-line machine code to place in IRAM and the attribute is silently ignored; (2) on ESP32-P4, `loadAndTranspose` (and thus `transpose_32_slices`) is called synchronously from `showPixelsImpl` in task context, not from any ISR or DMA interrupt handler, so flash-fetch latency is not a concern. `IRAM_ATTR` is only meaningful on the ESP32/S3 path where `loadAndTranspose` is called from the DMA ISR.
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-05T21:23:32.034Z
Learning: In hpwit/I2SClocklessLedDriver PR `#51` (commit 77853ba), Pixels copy constructor creates a non-owning view (localLedPointer=false, mapFunction=nullptr, arguments=nullptr); assignment operator is deleted. This is intentional: copy semantics produce a view, not a deep copy. The namespace wrapper for Pixel/Pixels/leddirection is out of scope for this PR and deferred.
Learnt from: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/**/*.{h,cpp} : For Platform branching, implement `CONFIG_IDF_TARGET_ESP32S3` (uses LCD_CAM + GDMA) and `CONFIG_IDF_TARGET_ESP32` (uses I2S0 + `esp_intr_alloc`) with hardware-specific register and peripheral code. `CONFIG_IDF_TARGET_ESP32P4` is referenced in `platformio.ini` but only virtual driver path is implemented.
|
@coderabbitai resume and review |
|
🧠 Learnings used✅ Actions performedReviews resumed. Review triggered.
|
|
@coderabbitai are you currently reviewing latest commit? |
🧩 Analysis chain🏁 Script executed: Length of output: 7277
🧠 Learnings used✅ Actions performedReview triggered.
|
|
@coderabbitai. you said you where reviewing latest commit but I don't see anything coming so |
|
🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/I2SClocklessLedDriver.h (1)
206-206:⚠️ Potential issue | 🔴 CriticalDon’t advertise 20 physical outputs on a 16-lane data path.
MAX_PINSnow letsinitled()accept up to 20 strips, but the physical driver path is still 16-wide:Lines::bytesis 16 bytes,signalsIDonly has 16 entries, andtransposeColorChannel()insrc/esp32-d0s3_i2s_impl.honly packs 16 lanes. On ESP32/S3, 17-20 strips will walk past fixed buffers instead of failing fast. Either keep the physical-driver limit at 16 or make the accepted strip count target-specific before storing it.As per coding guidelines "Hardware-specific branching should use CONFIG_IDF_TARGET_ESP32, CONFIG_IDF_TARGET_ESP32S3, or CONFIG_IDF_TARGET_ESP32P4 build-time defines".
Also applies to: 1427-1429
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` at line 206, The MAX_PINS change exposes up to 20 strips while the underlying driver is only 16-wide (Lines::bytes = 16, signalsID length 16, transposeColorChannel() packs 16 lanes), so update initled() to enforce a compile-time target-specific cap (use CONFIG_IDF_TARGET_ESP32 / CONFIG_IDF_TARGET_ESP32S3 / CONFIG_IDF_TARGET_ESP32P4) and reject or clamp requested pin counts above 16 for ESP32/S3, or revert MAX_PINS to 16; specifically adjust the logic that stores the requested pin count (refer to MAX_PINS and initled()), and ensure corresponding code paths that use Lines::bytes, signalsID and transposeColorChannel() are safe for the enforced limit; apply the same fix to the other occurrences noted around the later MAX_PINS usage (the block referenced at 1427-1429).
♻️ Duplicate comments (4)
src/I2SClocklessLedDriver.h (2)
1426-1445:⚠️ Potential issue | 🔴 CriticalReject invalid channel layouts before persisting them.
The
ColorArrangementoverloads always pass a legal(channelsPerLight, offsets...)tuple, but this public overload accepts arbitrary values and forwards them straight intoapplyConfiguration(). Invalid or duplicate offsets can then index past the active channel range inrgbwBufferMapping()and the transpose path (for example the fixedmapped[5]buffer insrc/esp32-d0s3_i2s_impl.h). Please add a shared validator here and reuse it fromupdateDriver()before touching driver state.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 1426 - 1445, The initled public API currently accepts arbitrary channelsPerLight and offset values and forwards them directly to applyConfiguration(), allowing invalid or duplicate offsets that can index past active channels in rgbwBufferMapping() and the transpose path (e.g., the fixed mapped[5] access); add a shared validator function (e.g., validateChannelLayout or reuse the validation used by updateDriver()) and call it at the start of initled to verify channelsPerLight and each offset (no duplicates, offsets < channelsPerLight, channelsPerLight within allowed range) before mutating driver state, and if validation fails log an error and return without calling applyConfiguration() so state isn’t persisted with an invalid layout.
1313-1337:⚠️ Potential issue | 🔴 CriticalP4 builds without PARLIO still reference methods that have no definition.
esp32-p4_parlio_impl.his only included whenHAS_PARLIO_DRIVER == 1, but the P4 show path and theloadAndTranspose()/hwStart()/hwStop()declarations remain visible even whenHAS_PARLIO_DRIVER == 0. On an ESP32-P4 build with older IDF,showPixelsImpl()still emits calls to those methods, leaving unresolved symbols at link time. Guard the declarations/call sites withHAS_PARLIO_DRIVERtoo, or provide stub definitions that fail initialization cleanly.Also applies to: 1532-1544
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.h` around lines 1313 - 1337, The ESP32-P4 show path references PARLIO-specific symbols that may be undefined when HAS_PARLIO_DRIVER == 0; wrap the P4-only calls and declarations (e.g., ensureParlioTxUnitInitialized(), loadAndTranspose(), hwStart(), hwStop(), and the showPixelsImpl P4 branch) in `#if` HAS_PARLIO_DRIVER guards so they are omitted when PARLIO is disabled, or alternatively add local stub implementations that return a clean failure (so showPixelsImpl can bail gracefully) to avoid unresolved link symbols; ensure the waiter-release logic (wasWaitingtofinish, waitDisp, xSemaphoreGive) remains correct in both guarded and stubbed paths.src/I2SClocklessLedDriver.cpp (1)
229-245:⚠️ Potential issue | 🟠 MajorRoll back partial
initBuffers()allocations on the first failure path.Several branches return after some buffers are already allocated in the same call. For example, an OOM in
transferBuffers[i]or theHAS_PARLIO_DRIVER == 0P4 branch leavestarget/hmap/earlier DMA or PARLIO buffers hanging until a later teardown. Please funnel these exits through a single cleanup path so a failed re-init does not permanently consume heap.Also applies to: 261-279, 338-363
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/I2SClocklessLedDriver.cpp` around lines 229 - 245, The initBuffers() routine currently returns early on allocation failures (e.g., when allocating target, hmap, transferBuffers[], or branch-specific buffers like PARLIO/DMA) without freeing previously allocated buffers; modify initBuffers() to funnel all failure exits through a single cleanup path (or call a helper like cleanupBuffers()) that frees target, hmap, all transferBuffers entries that were allocated, any DMA/PARLIO buffers, and then sets initErrorOccurred=true before returning; update the branches guarded by HARDWARESPRITES, __HARDWARE_MAP, HAS_PARLIO_DRIVER, and the allocation loop for transferBuffers to jump to that unified cleanup on any malloc failure rather than returning directly.docs/developer/workinprogress.md (1)
45-49:⚠️ Potential issue | 🟡 MinorUpdate the P4 lifecycle sections to match the current lazy-init flow.
These sections still describe PARLIO as being configured eagerly from
initLedImpl()/hwInit(), but the code now does a no-ophwInit()on P4, preparesp4ConfigininitBuffers(), and creates/enables the TX unit lazily inensureParlioTxUnitInitialized()on the firstshowPixels()call. Leaving the old flow here will send the next refactor to the wrong seam.As per coding guidelines "ESP32P4 platform should use PARLIO TX peripheral defined in src/parlio_p4.h/.cpp with lazy configuration on first showPixels() call".
Also applies to: 179-180, 215-216, 338-348
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/workinprogress.md` around lines 45 - 49, The lifecycle docs still describe eager PARLIO setup; update the P4 sections to reflect the current lazy-init flow: note that initLedImpl()/hwInit() for ESP32P4 are now no-ops (hwInit does not configure PARLIO), initBuffers() prepares the p4Config and allocates PSRAM buffers but does not enable the TX unit, and the PARLIO TX peripheral is created/enabled lazily by ensureParlioTxUnitInitialized() on the first showPixels() call; revise all referenced paragraphs (including the ones mentioned) to mention these exact symbols: initLedImpl, hwInit, initBuffers, p4Config, ensureParlioTxUnitInitialized, and showPixels.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/developer/standardsandguidelines.md`:
- Line 75: Replace the possessive typo in the PR-flow example sentence: change
"Coderabbit has done it's reviews and additional commits are done" to use the
correct possessive "its" (i.e., "Coderabbit has done its reviews and additional
commits are done") so the document uses "its" instead of "it's".
In `@src/colorarrangement.h`:
- Around line 50-84: The switch in applyColorArrangement(ColorArrangement cArr,
uint8_t& channelsPerLight, uint8_t& offsetRed, uint8_t& offsetGreen, uint8_t&
offsetBlue, uint8_t& offsetWhite, uint8_t& offsetWhite2) lacks a default, so an
invalid ColorArrangement can leave channelsPerLight and offsets uninitialized;
add a defensive default branch (or a pre-switch initialization) that sets
channelsPerLight to 0 (or a safe value), sets all offsets (offsetRed,
offsetGreen, offsetBlue, offsetWhite, offsetWhite2) to UINT8_MAX, and optionally
log/ASSERT/handle the unexpected enum to avoid unsafe indexing later.
In `@src/I2SClocklessLedDriver.h`:
- Around line 824-829: The FULL_DMA/LOOP helpers (e.g., stopDisplayLoop(),
transposeAll(), loadAndTranspose(this) and any code dereferencing
dmaBuffersTransposed) are being compiled for ESP32-P4 when FULL_DMA_BUFFER is
defined but those symbols only exist on ESP32/ESP32S3; wrap these helper
definitions/uses in the same ESP32/S3 target guard (e.g., `#if`
defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3)) so they
are excluded on P4, or alternatively emit a compile-time `#error` if
FULL_DMA_BUFFER is set for P4; apply the same guard changes to the other
affected helper blocks noted (around the other occurrences mentioned).
---
Outside diff comments:
In `@src/I2SClocklessLedDriver.h`:
- Line 206: The MAX_PINS change exposes up to 20 strips while the underlying
driver is only 16-wide (Lines::bytes = 16, signalsID length 16,
transposeColorChannel() packs 16 lanes), so update initled() to enforce a
compile-time target-specific cap (use CONFIG_IDF_TARGET_ESP32 /
CONFIG_IDF_TARGET_ESP32S3 / CONFIG_IDF_TARGET_ESP32P4) and reject or clamp
requested pin counts above 16 for ESP32/S3, or revert MAX_PINS to 16;
specifically adjust the logic that stores the requested pin count (refer to
MAX_PINS and initled()), and ensure corresponding code paths that use
Lines::bytes, signalsID and transposeColorChannel() are safe for the enforced
limit; apply the same fix to the other occurrences noted around the later
MAX_PINS usage (the block referenced at 1427-1429).
---
Duplicate comments:
In `@docs/developer/workinprogress.md`:
- Around line 45-49: The lifecycle docs still describe eager PARLIO setup;
update the P4 sections to reflect the current lazy-init flow: note that
initLedImpl()/hwInit() for ESP32P4 are now no-ops (hwInit does not configure
PARLIO), initBuffers() prepares the p4Config and allocates PSRAM buffers but
does not enable the TX unit, and the PARLIO TX peripheral is created/enabled
lazily by ensureParlioTxUnitInitialized() on the first showPixels() call; revise
all referenced paragraphs (including the ones mentioned) to mention these exact
symbols: initLedImpl, hwInit, initBuffers, p4Config,
ensureParlioTxUnitInitialized, and showPixels.
In `@src/I2SClocklessLedDriver.cpp`:
- Around line 229-245: The initBuffers() routine currently returns early on
allocation failures (e.g., when allocating target, hmap, transferBuffers[], or
branch-specific buffers like PARLIO/DMA) without freeing previously allocated
buffers; modify initBuffers() to funnel all failure exits through a single
cleanup path (or call a helper like cleanupBuffers()) that frees target, hmap,
all transferBuffers entries that were allocated, any DMA/PARLIO buffers, and
then sets initErrorOccurred=true before returning; update the branches guarded
by HARDWARESPRITES, __HARDWARE_MAP, HAS_PARLIO_DRIVER, and the allocation loop
for transferBuffers to jump to that unified cleanup on any malloc failure rather
than returning directly.
In `@src/I2SClocklessLedDriver.h`:
- Around line 1426-1445: The initled public API currently accepts arbitrary
channelsPerLight and offset values and forwards them directly to
applyConfiguration(), allowing invalid or duplicate offsets that can index past
active channels in rgbwBufferMapping() and the transpose path (e.g., the fixed
mapped[5] access); add a shared validator function (e.g., validateChannelLayout
or reuse the validation used by updateDriver()) and call it at the start of
initled to verify channelsPerLight and each offset (no duplicates, offsets <
channelsPerLight, channelsPerLight within allowed range) before mutating driver
state, and if validation fails log an error and return without calling
applyConfiguration() so state isn’t persisted with an invalid layout.
- Around line 1313-1337: The ESP32-P4 show path references PARLIO-specific
symbols that may be undefined when HAS_PARLIO_DRIVER == 0; wrap the P4-only
calls and declarations (e.g., ensureParlioTxUnitInitialized(),
loadAndTranspose(), hwStart(), hwStop(), and the showPixelsImpl P4 branch) in
`#if` HAS_PARLIO_DRIVER guards so they are omitted when PARLIO is disabled, or
alternatively add local stub implementations that return a clean failure (so
showPixelsImpl can bail gracefully) to avoid unresolved link symbols; ensure the
waiter-release logic (wasWaitingtofinish, waitDisp, xSemaphoreGive) remains
correct in both guarded and stubbed paths.
🪄 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: 62910b01-7db2-4e69-ab7f-b953d2ab23ce
⛔ Files ignored due to path filters (1)
docs/developer/PRflow.pngis excluded by!**/*.png
📒 Files selected for processing (13)
CLAUDE.mddocs/developer/developer.mddocs/developer/standardsandguidelines.mddocs/developer/workinprogress.mddocs/enduser/enduser.mddocs/index.mdplatformio.inisrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/colorarrangement.hsrc/esp32-d0s3_i2s_impl.hsrc/esp32-p4_parlio_impl.hsrc/helper.h
Fixed: 1. standardsandguidelines.md — "it's" → "its" (possessive typo, confirmed at line 75). 2. colorarrangement.h — Added pre-switch initialization of channelsPerLight=0 and offsetRed/Green/Blue=UINT8_MAX before the switch(cArr). Without this, an unrecognised enum value left three of the five output parameters uninitialised, causing potential out-of-bounds access in rgbwBufferMapping(). 3. I2SClocklessLedDriver.h — FULL_DMA_BUFFER + P4 — Added #error that catches the invalid combination at compile time. Confirmed: dmaBuffersTransposed (used by deleteBuffers() and the FULL_DMA_BUFFER helpers) is only declared for ESP32/S3; compiling with both flags set on P4 would fail with confusing symbol errors rather than a clear message. 4. I2SClocklessLedDriver.h — validateChannelLayout() — New static bool method checks channelsPerLight (3–5), all active offsets are within bounds and mutually distinct. Called at the top of initled() before any state is mutated. 5. I2SClocklessLedDriver.h — initled() hardware strip cap — Added #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S3 guard rejecting numStrips > 16. Confirmed: Lines::bytes[16], signalsID[16] (only 16 entries despite MAX_PINS=20), and transposeColorChannel() all hard-code 16 lanes; allowing 17–20 strips causes silent buffer overflow. 6. I2SClocklessLedDriver.h / .cpp — initBuffers() partial-allocation cleanup — Added deleteBuffers() + restore of initErrorOccurred=true after a failed initBuffers() in both initled() and updateDriver(). Without this, partial heap allocations (e.g. target or transferBuffers[0..k-1]) were leaked until the driver was explicitly destroyed. 7. workinprogress.md — Updated P4 call-path docs to reflect the lazy-init flow: hwInit() is now a no-op, initBuffers() only populates p4Config/buffers, ensureParlioTxUnitInitialized() creates/enables the TX unit on the first showPixels() call. Not fixed (findings 7/8 from duplicate section): The HAS_PARLIO_DRIVER==0 P4 show-path finding was skipped — the existing initSuccess=false early-return in showPixelsImpl() already makes the unreachable calls safe at runtime, and the P4 impl provides compiled-in stubs regardless of HAS_PARLIO_DRIVER, so there is no compilation or link failure to fix.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
docs/developer/standardsandguidelines.md (1)
75-75:⚠️ Potential issue | 🟡 MinorUse consistent product naming (“CodeRabbit”).
Line 75 still uses
Coderabbit; elsewhere the doc usesCodeRabbit. Please normalize for consistency/readability.📝 Proposed doc fix
-- see [this PR](https://github.com/hpwit/I2SClocklessLedDriver/pull/54) : Coderabbit has done its reviews and additional commits are done +- see [this PR](https://github.com/hpwit/I2SClocklessLedDriver/pull/54): CodeRabbit has done its reviews and additional commits are done🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/developer/standardsandguidelines.md` at line 75, Update the product name on the referenced line by replacing the lowercase variant "Coderabbit" with the standardized "CodeRabbit" to match the rest of the document; locate the exact text fragment "see [this PR](https://github.com/hpwit/I2SClocklessLedDriver/pull/54) : Coderabbit has done its reviews and additional commits are done" and change only the product name token to "CodeRabbit" to normalize naming across the document.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/developer/standardsandguidelines.md`:
- Line 25: Update the repo layout docs to use the correct file-name casing:
replace "HardwareSprite.h/.cpp" with "hardwareSprite.h/.cpp" (the actual
implemented files are src/hardwareSprite.h and src/hardwareSprite.cpp) so the
docs match the repo and won't mislead users on case-sensitive systems.
In `@src/I2SClocklessLedDriver.h`:
- Around line 1593-1606: Declarations for the ESP32-P4 methods
(loadAndTranspose, hwStart, hwStop) are unguarded causing linker errors when
HAS_PARLIO_DRIVER==0; wrap those declarations with `#if` CONFIG_IDF_TARGET_ESP32P4
&& HAS_PARLIO_DRIVER so they are only declared when the parlio implementation is
available, and likewise guard the P4-specific branch inside showPixelsImpl()
(the block that calls loadAndTranspose/hwStart/hwStop) so that when
CONFIG_IDF_TARGET_ESP32P4 && !HAS_PARLIO_DRIVER the function instead
returns/aborts with a clear compile-time/runtime error message; reference the
symbols loadAndTranspose, hwStart, hwStop, showPixelsImpl,
CONFIG_IDF_TARGET_ESP32P4, and HAS_PARLIO_DRIVER when making these conditional
changes.
---
Duplicate comments:
In `@docs/developer/standardsandguidelines.md`:
- Line 75: Update the product name on the referenced line by replacing the
lowercase variant "Coderabbit" with the standardized "CodeRabbit" to match the
rest of the document; locate the exact text fragment "see [this
PR](https://github.com/hpwit/I2SClocklessLedDriver/pull/54) : Coderabbit has
done its reviews and additional commits are done" and change only the product
name token to "CodeRabbit" to normalize naming across the document.
🪄 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: b7d7517e-c9ee-428f-bd9d-774a8fe64f00
📒 Files selected for processing (5)
docs/developer/standardsandguidelines.mddocs/developer/workinprogress.mdsrc/I2SClocklessLedDriver.cppsrc/I2SClocklessLedDriver.hsrc/colorarrangement.h
✅ Files skipped from review due to trivial changes (2)
- src/colorarrangement.h
- docs/developer/workinprogress.md
🚧 Files skipped from review as they are similar to previous changes (1)
- src/I2SClocklessLedDriver.cpp
| parlio_p4.h/.cpp ESP32-P4 PARLIO implementation | ||
| pixeltypes.h Pixel / Pixels types | ||
| framebuffer.h Double-buffer helper | ||
| HardwareSprite.h/.cpp Optional sprite overlay |
There was a problem hiding this comment.
Fix file-name casing in the repo layout block.
HardwareSprite.h/.cpp likely does not match the actual file names (hardwareSprite.h/.cpp). On case-sensitive systems, this can mislead contributors and break quick file navigation from docs.
📝 Proposed doc fix
- HardwareSprite.h/.cpp Optional sprite overlay
+ hardwareSprite.h/.cpp Optional sprite overlayBased on learnings, HARDWARESPRITES is implemented in src/hardwareSprite.h and src/hardwareSprite.cpp.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| HardwareSprite.h/.cpp Optional sprite overlay | |
| hardwareSprite.h/.cpp Optional sprite overlay |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/developer/standardsandguidelines.md` at line 25, Update the repo layout
docs to use the correct file-name casing: replace "HardwareSprite.h/.cpp" with
"hardwareSprite.h/.cpp" (the actual implemented files are src/hardwareSprite.h
and src/hardwareSprite.cpp) so the docs match the repo and won't mislead users
on case-sensitive systems.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/enduser/enduser.md`:
- Around line 334-349: Update the P4-specific docs to reflect actual behavior:
remove or correct the “First-frame warm-up” claim that a first showPixels() is
only config and second sends the first frame—on P4 the PARLIO TX is initialized
and loadAndTranspose()/hwStart()/hwStop() are invoked in the same showPixels()
call so the frame is configured/sent in that invocation; also change the wording
around FULL_DMA_BUFFER to state that src/I2SClocklessLedDriver.h now hard-fails
the build with `#error` on ESP32-P4 (rather than merely being “unavailable”), and
keep the existing notes about PARLIO/PARLIO_TX, PSRAM, and PARLIO_AUTO_OVERCLOCK
as-is.
In `@src/I2SClocklessLedDriver.h`:
- Around line 1316-1344: The ESP32P4 branch currently ignores displayMode and
always calls hwStop (which blocks via parlio_tx_unit_wait_all_done), causing
NO_WAIT to block; fix showPixels by consulting displayMode: after
ensureParlioTxUnitInitialized() and successful loadAndTranspose(), always call
hwStart(), but only call hwStop() if displayMode==WAIT (or equivalent WAIT
constant); for NO_WAIT return immediately (leaving isDisplaying true or manage
state so callers expecting no-wait behavior work) and ensure
wasWaitingtofinish/waitDisp logic is only used for WAIT paths — update handling
around ensureParlioTxUnitInitialized, loadAndTranspose, hwStart, hwStop and the
waiter semaphore so NO_WAIT does not block.
- Around line 1433-1482: validateChannelLayout currently permits arbitrary
distinct offsets but FULL_DMA writers like setPixelinBufferByStrip assume
canonical component ordering and use a fixed uint8_t colors[3], causing
overflow/misordered writes into dmaBuffersTransposed; fix by restricting
validateChannelLayout (or adding a conditional branch when FULL_DMA is enabled)
to only accept layouts that match the canonical slot order used by the FULL_DMA
code (e.g., for channelsPerLight==3 require offsets {R=0,G=1,B=2}; for 4 require
RGB then W at the last slot; for 5 require RGB then W then W2), or alternatively
refactor setPixelinBufferByStrip/dma full-DMA writers to index components by the
provided offsets (use offsetRed/offsetGreen/offsetBlue/offsetWhite/offsetWhite2
instead of assuming fixed positions and replace fixed-size colors[] with a
bounds-checked buffer); update validateChannelLayout, initled usage, and
setPixelinBufferByStrip accordingly so the accepted layouts are always encodable
by FULL_DMA writers.
🪄 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: 94ac700e-5eb9-4ba7-b4d1-c276f63a5544
📒 Files selected for processing (8)
CLAUDE.mddocs/developer/standardsandguidelines.mddocs/developer/workinprogress.mddocs/enduser/enduser.mdplatformio.inisrc/I2SClocklessLedDriver.hsrc/esp32-d0s3_i2s_impl.hsrc/esp32-p4_parlio_impl.h
✅ Files skipped from review due to trivial changes (3)
- platformio.ini
- docs/developer/standardsandguidelines.md
- docs/developer/workinprogress.md
🚧 Files skipped from review as they are similar to previous changes (1)
- CLAUDE.md
| ### P4-specific notes | ||
|
|
||
| - **No I2S peripheral** — PARLIO TX is used instead. No `FULL_DMA_BUFFER`, `LOOP`, or `showPixelsFromBuffer()` modes are available on P4. | ||
| - **PSRAM** — the dual waveform buffers (~656 KB total for the maximum 1024 LEDs × 16 outputs × 5 channels) are allocated in PSRAM+DMA. A P4 board with PSRAM is strongly recommended for more than a few strips. | ||
| - **Adaptive clock** — define `PARLIO_AUTO_OVERCLOCK` to enable automatic clock scaling (1.2 MHz for ≤256 LEDs/output, 1.1 MHz for ≤512, 800 kHz otherwise). This can improve FPS with short strips. | ||
| - **First-frame warm-up** — the PARLIO unit takes one `showPixels()` call to configure when the topology changes (number of outputs or LEDs-per-output). The second call sends the first visible frame. | ||
|
|
||
| --- | ||
|
|
||
| ## Compile-time Options | ||
|
|
||
| Set these **before** `#include "I2SClocklessLedDriver.h"`: | ||
|
|
||
| | Define | Default | Effect | | ||
| |--------|---------|--------| | ||
| | `FULL_DMA_BUFFER` | off | Enable full pre-transposed DMA buffer mode | | ||
| | `FULL_DMA_BUFFER` | off | Enable full pre-transposed DMA buffer mode (ESP32/S3 only) | |
There was a problem hiding this comment.
Update the P4 docs to match the actual behavior.
This section currently says topology changes need one warm-up showPixels() before the first visible frame, but the P4 path still initializes the TX unit and then calls loadAndTranspose()/hwStart()/hwStop() in that same invocation. It also reads as if FULL_DMA_BUFFER is merely unavailable, while src/I2SClocklessLedDriver.h now hard-fails the build with #error on ESP32-P4. Tightening the wording here will save users from discarding the first frame or chasing an unexpected compile failure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/enduser/enduser.md` around lines 334 - 349, Update the P4-specific docs
to reflect actual behavior: remove or correct the “First-frame warm-up” claim
that a first showPixels() is only config and second sends the first frame—on P4
the PARLIO TX is initialized and loadAndTranspose()/hwStart()/hwStop() are
invoked in the same showPixels() call so the frame is configured/sent in that
invocation; also change the wording around FULL_DMA_BUFFER to state that
src/I2SClocklessLedDriver.h now hard-fails the build with `#error` on ESP32-P4
(rather than merely being “unavailable”), and keep the existing notes about
PARLIO/PARLIO_TX, PSRAM, and PARLIO_AUTO_OVERCLOCK as-is.
| #elif CONFIG_IDF_TARGET_ESP32P4 | ||
| #if HAS_PARLIO_DRIVER | ||
| if (!ensureParlioTxUnitInitialized()) { | ||
| // TX unit could not be created — release any waiter and bail | ||
| if (wasWaitingtofinish && waitDisp != NULL) { | ||
| wasWaitingtofinish = false; | ||
| xSemaphoreGive(waitDisp); | ||
| } | ||
| isDisplaying = false; | ||
| return; | ||
| } | ||
| #else | ||
| ESP_LOGE(TAG, "PARLIO driver unavailable — ESP-IDF v5.1+ required"); | ||
| isDisplaying = false; | ||
| return; | ||
| #endif | ||
| if (loadAndTranspose()) { | ||
| hwStart(); | ||
| hwStop(); | ||
| } else { | ||
| // loadAndTranspose failed (buffer too small) — release waiter | ||
| if (wasWaitingtofinish && waitDisp != NULL) { | ||
| wasWaitingtofinish = false; | ||
| xSemaphoreGive(waitDisp); | ||
| } | ||
| } | ||
| // If loadAndTranspose fails (buffer too small) the frame is skipped silently; | ||
| // the error was already logged inside loadAndTranspose(). | ||
| isDisplaying = false; |
There was a problem hiding this comment.
P4 currently ignores NO_WAIT and always blocks.
displayMode is never consulted on this branch. Because hwStop() waits on parlio_tx_unit_wait_all_done(portMAX_DELAY), showPixels(NO_WAIT) blocks until the transfer completes exactly like WAIT. That breaks existing caller logic that selects NO_WAIT generically across targets.
Suggested direction
`#elif` CONFIG_IDF_TARGET_ESP32P4
+ if (displayMode != WAIT) {
+ ESP_LOGE(TAG, "ESP32-P4 currently supports only WAIT mode");
+ if (wasWaitingtofinish && waitDisp != NULL) {
+ wasWaitingtofinish = false;
+ xSemaphoreGive(waitDisp);
+ }
+ isDisplaying = false;
+ return;
+ }
if (loadAndTranspose()) {
hwStart();
hwStop();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/I2SClocklessLedDriver.h` around lines 1316 - 1344, The ESP32P4 branch
currently ignores displayMode and always calls hwStop (which blocks via
parlio_tx_unit_wait_all_done), causing NO_WAIT to block; fix showPixels by
consulting displayMode: after ensureParlioTxUnitInitialized() and successful
loadAndTranspose(), always call hwStart(), but only call hwStop() if
displayMode==WAIT (or equivalent WAIT constant); for NO_WAIT return immediately
(leaving isDisplaying true or manage state so callers expecting no-wait behavior
work) and ensure wasWaitingtofinish/waitDisp logic is only used for WAIT paths —
update handling around ensureParlioTxUnitInitialized, loadAndTranspose, hwStart,
hwStop and the waiter semaphore so NO_WAIT does not block.
| /** Validate that channelsPerLight and per-channel offsets describe a legal, | ||
| * non-overlapping wire layout that fits within the mapped[] array (size 5). | ||
| * Returns false and logs an error on any violation; true when valid. */ | ||
| static bool validateChannelLayout(uint8_t channelsPerLight, uint8_t offsetRed, uint8_t offsetGreen, uint8_t offsetBlue, uint8_t offsetWhite, uint8_t offsetWhite2) { | ||
| if (channelsPerLight < 3 || channelsPerLight > 5) { | ||
| ESP_LOGE(TAG, "validateChannelLayout: channelsPerLight=%u must be 3-5", channelsPerLight); | ||
| return false; | ||
| } | ||
| if (offsetRed >= channelsPerLight || offsetGreen >= channelsPerLight || offsetBlue >= channelsPerLight) { | ||
| ESP_LOGE(TAG, "validateChannelLayout: RGB offsets must be < channelsPerLight(%u)", channelsPerLight); | ||
| return false; | ||
| } | ||
| if (offsetRed == offsetGreen || offsetRed == offsetBlue || offsetGreen == offsetBlue) { | ||
| ESP_LOGE(TAG, "validateChannelLayout: RGB offsets must be distinct"); | ||
| return false; | ||
| } | ||
| if (offsetWhite != UINT8_MAX) { | ||
| if (offsetWhite >= channelsPerLight || offsetWhite == offsetRed || offsetWhite == offsetGreen || offsetWhite == offsetBlue) { | ||
| ESP_LOGE(TAG, "validateChannelLayout: offsetWhite=%u invalid or clashes with RGB", offsetWhite); | ||
| return false; | ||
| } | ||
| } | ||
| if (offsetWhite2 != UINT8_MAX) { | ||
| if (offsetWhite2 >= channelsPerLight || offsetWhite2 == offsetRed || offsetWhite2 == offsetGreen || offsetWhite2 == offsetBlue) { | ||
| ESP_LOGE(TAG, "validateChannelLayout: offsetWhite2=%u invalid or clashes with RGB", offsetWhite2); | ||
| return false; | ||
| } | ||
| if (offsetWhite != UINT8_MAX && offsetWhite2 == offsetWhite) { | ||
| ESP_LOGE(TAG, "validateChannelLayout: offsetWhite2 must differ from offsetWhite"); | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| void initled(uint8_t* leds, uint8_t* pinsq, uint16_t* sizes, uint8_t numStrips, uint8_t channelsPerLight, uint8_t offsetRed, uint8_t offsetGreen, uint8_t offsetBlue, uint8_t offsetWhite = UINT8_MAX, uint8_t offsetWhite2 = UINT8_MAX, bool extractWhiteFromRGB = false) { | ||
| if (pinsq == nullptr || sizes == nullptr || numStrips == 0 || numStrips > MAX_PINS) { | ||
| ESP_LOGE(TAG, "initled: invalid args numStrips=%u sizes=%p pinsq=%p", numStrips, (void*)sizes, (void*)pinsq); | ||
| return; | ||
| } | ||
| // ESP32/S3: hardware parallel output is 16-lane wide; P4 limit is checked in initBuffers(). | ||
| #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S3 | ||
| if (numStrips > 16) { | ||
| ESP_LOGE(TAG, "initled: numStrips=%u exceeds 16-lane hardware limit on ESP32/S3", numStrips); | ||
| return; | ||
| } | ||
| #endif | ||
| if (!validateChannelLayout(channelsPerLight, offsetRed, offsetGreen, offsetBlue, offsetWhite, offsetWhite2)) { | ||
| return; // error already logged inside validateChannelLayout | ||
| } |
There was a problem hiding this comment.
Don't accept channel layouts that the FULL_DMA writers can't encode.
validateChannelLayout() now accepts any distinct offsets within channelsPerLight, but setPixelinBufferByStrip() still assumes component slots are [RGB][W][W2] and even stores RGB into uint8_t colors[3]. A layout that is currently considered valid here can therefore overflow that array and write the wrong component order into dmaBuffersTransposed.
Either generalize the FULL_DMA direct-write helpers to address components by offset, or restrict validation so only canonical slot orders are accepted when those helpers are available.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/I2SClocklessLedDriver.h` around lines 1433 - 1482, validateChannelLayout
currently permits arbitrary distinct offsets but FULL_DMA writers like
setPixelinBufferByStrip assume canonical component ordering and use a fixed
uint8_t colors[3], causing overflow/misordered writes into dmaBuffersTransposed;
fix by restricting validateChannelLayout (or adding a conditional branch when
FULL_DMA is enabled) to only accept layouts that match the canonical slot order
used by the FULL_DMA code (e.g., for channelsPerLight==3 require offsets
{R=0,G=1,B=2}; for 4 require RGB then W at the last slot; for 5 require RGB then
W then W2), or alternatively refactor setPixelinBufferByStrip/dma full-DMA
writers to index components by the provided offsets (use
offsetRed/offsetGreen/offsetBlue/offsetWhite/offsetWhite2 instead of assuming
fixed positions and replace fixed-size colors[] with a bounds-checked buffer);
update validateChannelLayout, initled usage, and setPixelinBufferByStrip
accordingly so the accepted layouts are always encodable by FULL_DMA writers.
|
@coderabbitai, I am about to merge this PR. Please produce three outputs:
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 26320 🏁 Script executed: Length of output: 6275 🏁 Script executed: Length of output: 7171
PR
|
| Area | Before | After |
|---|---|---|
| Hardware entrypoints | i2sInit, i2sStart, i2sStop |
hwInit, hwStart, hwStop |
| Per-strip ISR buffer | dmaBuffersTampon[] |
transferBuffers[] |
| Color channel params | nbComponents, pR/pG/pB/pW/pW2 |
channelsPerLight, offsetRed/Green/Blue/White/White2 |
| Platform-specific impl | Inline in I2SClocklessLedDriver.h |
Extracted to esp32-d0s3_i2s_impl.h and esp32-p4_parlio_impl.h |
| Init pipeline | Monolithic | Split into applyConfiguration() → initBuffers() → deleteBuffers() |
| Color-order abstraction | ColorArrangement enum inline |
New src/colorarrangement.h with applyColorArrangement() |
ESP32-P4 PARLIO driver (esp32-p4_parlio_impl.h)
- Bit-transposition via a 256-entry LUT into 32-slice PARLIO waveform bitmasks
- Lazy
ensureParlioTxUnitInitialized()— TX unit created on firstshowPixels()call - Chunked transmit for frames larger than 65 535 bytes (PARLIO DMA limit)
- Synchronous
hwStop()— waits for all chunks viaparlio_tx_unit_wait_all_done; no ISR required - Ping-pong buffer selection
- RGBCCT warm-white LUT support with zero-padding for variable strip lengths
API additions/changes
- New
initled()overload usingchannelsPerLight+ offset parameters - New
show()alias forshowPixels() - New
validateChannelLayout()validates offset parameters before any state mutation updateDriver()signature updated to match new channel-offset model#errorguard that catchesFULL_DMA_BUFFER+CONFIG_IDF_TARGET_ESP32P4at compile time
Final-commit bug fixes (ed5d315)
colorarrangement.h: pre-switch default initialisation prevents UB on unrecognised enum valuesinitBuffers()partial-allocation path now callsdeleteBuffers()and restoresinitErrorOccurred = trueto prevent heap leaks- Strip cap:
numStrips > 16rejected at runtime on ESP32/ESP32-S3 (buffer overflow prevention) isDisplaying = falseset on all early-exit paths inshowPixelsImpl()loadAndTranspose()returnsbool;hwStart()/hwStop()only called on success
2. Merge Recommendation
✅ RECOMMEND MERGE — all previously identified blockers are resolved.
The critical issues flagged across reviews are confirmed fixed:
static loadAndTransposeconflict → guarded with#ifndef CONFIG_IDF_TARGET_ESP32P4driver/parlio_tx.hmissing include → now presentinitTransferBuffers(this)call-signature mismatch → removedloadAndTransposeredefinition /hwStopredefinition → resolved via class-method model + platform guardsisDisplayingleak on early exits → fixed- Partial
initBuffers()allocation cleanup → added validateChannelLayout()guard → added
The P4 path is synchronous (no ISR), making its correctness surface smaller than the ESP32/S3 DMA path. The ESP32/S3 path is largely pre-existing and well-tested.
3. Prioritised Follow-Up Actions
🟠 High — Worth a follow-up issue before the next feature PR
A. Verify the semSync double-give in esp32-d0s3_i2s_impl.h
The review of commit 7402d8a flagged a potential duplicate xSemaphoreGiveFromISR(driver->semSync, ...) at line 293 of esp32-d0s3_i2s_impl.h. There are now 4 xSemaphoreGiveFromISR calls in that file (lines 192, 195, 233, 293). If line 293 fires in the same ISR invocation as line 233 without a new xSemaphoreTake, semSync can be given twice, causing waitSync() to return one frame early.
B. developer.md stale "nothing is freed" comment
A review comment flagged that the documentation at line 91 of docs/developer/developer.md says "No I2S/DMA buffers were allocated, so nothing is freed" for the P4 path, but deleteDriver() / deleteBuffers() in I2SClocklessLedDriver.cpp does free p4Buffer1 and p4Buffer2.
🟡 Low — Nitpick follow-ups
C. The 26 open CodeRabbit review comments contain many stale entries from early commits (e.g., the parlio_p4.cpp file that no longer exists). These can be batch-resolved.
D. docs/developer/standardsandguidelines.md code blocks still lack language specifiers (e.g., ```text). Minor markdown linting issue.
E. channelsPerLight > 5 support: the ESP32/S3 loadAndTranspose path in esp32-d0s3_i2s_impl.h still uses a hardcoded uint8_t mapped[5] and 5 hardcoded transposeColorChannel calls. This limits RGBCCT+ configurations to the P4 path only. (Tracked in long-term memory as a known gap.)
4. Ready-to-Paste AI Agent Prompt (for item A — semSync double-give)
Repository: hpwit/I2SClocklessLedDriver
Branch: esp32-p4-support
File: src/esp32-d0s3_i2s_impl.h
TASK: Audit the `interruptHandler` ISR in `esp32-d0s3_i2s_impl.h` for a potential double `xSemaphoreGiveFromISR(driver->semSync, ...)`.
BACKGROUND:
There are currently 4 `xSemaphoreGiveFromISR` calls in this file (lines 192, 195, 233, 293).
`semSync` is used by `waitSync()` to block until the next frame boundary.
If `semSync` is given twice in a single ISR firing (once at line 233 and once at line 293) without an intervening `xSemaphoreTake`, `waitSync()` returns one frame early.
STEPS:
1. Read the full `interruptHandler` function for both the ESP32 path (`#ifdef CONFIG_IDF_TARGET_ESP32`) and the ESP32-S3 path (`#elif CONFIG_IDF_TARGET_ESP32S3`).
2. For each of the 4 `xSemaphoreGiveFromISR` calls, identify:
a. Which semaphore is being given (`sem`, `waitDisp`, or `semSync`).
b. Under what condition it fires (e.g., `out_total_eof`, `framesync`, `transpose` flag, etc.).
c. Whether it is possible for the same semaphore to be given twice in one ISR invocation.
3. If a genuine double-give is found on `semSync`:
- Add a guard (e.g., `if (driver->isWaiting && driver->semSync != NULL)`) around the second give, OR
- Merge the two give paths so only one fires per ISR invocation.
4. If no double-give is possible (because the two conditions are mutually exclusive), add a comment explaining why they cannot both fire in the same invocation.
5. Produce a minimal diff limited to `src/esp32-d0s3_i2s_impl.h`.
[resolve]
🧠 Learnings used
Learnt from: MoonModules
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-08T10:40:10.745Z
Learning: In hpwit/I2SClocklessLedDriver (src/parlio_p4.cpp), `transpose_32_slices` is declared `inline` and must NOT be given `IRAM_ATTR`: (1) `inline` and `IRAM_ATTR` are incompatible — an inline function has no out-of-line machine code to place in IRAM and the attribute is silently ignored; (2) on ESP32-P4, `loadAndTranspose` (and thus `transpose_32_slices`) is called synchronously from `showPixelsImpl` in task context, not from any ISR or DMA interrupt handler, so flash-fetch latency is not a concern. `IRAM_ATTR` is only meaningful on the ESP32/S3 path where `loadAndTranspose` is called from the DMA ISR.
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-05T18:21:16.152Z
Learning: In the I2SClocklessLedDriver library (hpwit/I2SClocklessLedDriver), the user (ewowi) confirmed that the following are correctly implemented as of PR `#51` / commit 450d5c1: getStrip() bounds guard, getLengths() uint16_t* return type, createSubset clamping, frameBuffer null-deref protection with _offPixel sentinel and valid(), hardwareSprite NBSPRITE overflow guard, deleteDriver() nulling DMABuffersTampon, freeing target and _hmap, ISR semaphore logic, and main.cpp effect cycling. The namespace wrapper for struct Pixel / class Pixels / enum class leddirection in src/___pixeltypes.h is intentionally deferred as a missing feature (not a bug).
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 54
File: src/I2SClocklessLedDriver.cpp:33-37
Timestamp: 2026-04-11T16:00:12.799Z
Learning: In hpwit/I2SClocklessLedDriver (src/esp32-d0s3_i2s_impl.h), `loadAndTranspose` uses a hardcoded `uint8_t mapped[5]` temporary array and five hardcoded `transposeColorChannel` calls (indices 0-4). Supporting channelsPerLight > 5 requires: (1) changing `mapped[5]` to a VLA `mapped[channelsPerLight]`, (2) replacing the five hardcoded `transposeColorChannel` calls with a loop over `channelsPerLight`, and (3) generalising `rgbwBufferMapping` in `I2SClocklessLedDriver.h` which hardcodes `src[3]` for W and `src[4]` for W2. The ESP32-P4 path in `esp32-p4_parlio_impl.h` already uses VLAs and a dynamic loop so it is largely future-proof.
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 54
File: src/I2SClocklessLedDriver.cpp:266-279
Timestamp: 2026-04-11T16:01:05.990Z
Learning: In hpwit/I2SClocklessLedDriver (src/I2SClocklessLedDriver.cpp), the project maintainer (ewowi) considers OOM allocation failures on ESP32 as effectively unrecoverable system failures. Elaborate rollback/cleanup logic in initBuffers() for partial allocation failures is intentionally not included in production code. The preferred approach is either ESP-IDF's CONFIG_HEAP_ABORT_ON_ALLOCATION_FAILURE Kconfig option, or `#ifdef` DEBUG guards. Do not flag missing rollback/cleanup on allocation failure in initBuffers() as a major issue.
Learnt from: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/I2SClocklessLedDriver.{h,cpp} : Implement two DMA pipeline modes: **Ping-pong DMA** (default) with two small buffers filled incrementally by ISR (low RAM, CPU on each ISR call), and **Full DMA buffer** (enabled with `#define FULL_DMA_BUFFER`) that pre-transposes the entire frame for autonomous I2S operation. Full DMA buffer enables `showPixelsFirstTranspose()`, `showPixelsFromBuffer()`, and `showPixelsFromBuffer(LOOP)` methods.
Learnt from: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/**/*.{h,cpp} : ISR functions (`i2sStop`, `interruptHandler`) must be marked with `IRAM_ATTR` and use only ISR-safe FreeRTOS calls (`xSemaphoreGiveFromISR`, `portYIELD_FROM_ISR`). Do not make regular semaphore calls or CPU-intensive operations in ISR context.
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-10T12:35:37.089Z
Learning: In hpwit/I2SClocklessLedDriver, the project follows standard C++ practice: class declarations, type definitions, and inline helpers go in `.h` header files, while function/method implementations go in `.cpp` source files. This applies to `src/I2SClocklessLedDriver.h` (class declaration, inline helpers) and `src/I2SClocklessLedDriver.cpp` (method implementations such as `updateDriver()`, `deleteDriver()`, `initBuffers()`, `applyConfiguration()`). Do NOT flag `.cpp` implementations as violating a "header-first" rule — that rule does not apply here.
Learnt from: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/I2SClocklessLedDriver.{h,cpp} : Use FreeRTOS semaphores on the driver object with specific purposes: `sem` blocks `showPixels(WAIT)` until transfer completes, `semSync` signals frame-sync for `waitSync()`, and `waitDisp` is lazy-created in `updateDriver()` to wait for in-flight DMA before reconfiguration. `waitDisp` must be released from ISR via `xSemaphoreGiveFromISR`.
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-05T21:23:32.034Z
Learning: In hpwit/I2SClocklessLedDriver PR `#51`, tools/patch_compile_db.py uses shutil.copy2 to copy xtensa base headers directly into the ~/.platformio/packages/framework-arduinoespressif32-libs/ package tree (in-place), which corrupts the PlatformIO installation and causes "xthal_set_intset was not declared in this scope" errors in subsequent pio run builds. Fix: rm -rf ~/.platformio/packages/framework-arduinoespressif32-libs to restore clean packages. Permanent fix: copy stubs into OUTPUT_DIR/xtensa_stubs/ instead and use -isystem pointing there.
Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: :0-0
Timestamp: 2026-04-05T21:23:32.034Z
Learning: In hpwit/I2SClocklessLedDriver PR `#51` (commit 77853ba), Pixels copy constructor creates a non-owning view (localLedPointer=false, mapFunction=nullptr, arguments=nullptr); assignment operator is deleted. This is intentional: copy semantics produce a view, not a deep copy. The namespace wrapper for Pixel/Pixels/leddirection is out of scope for this PR and deferred.
Learnt from: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/**/*.{h,cpp} : For Platform branching, implement `CONFIG_IDF_TARGET_ESP32S3` (uses LCD_CAM + GDMA) and `CONFIG_IDF_TARGET_ESP32` (uses I2S0 + `esp_intr_alloc`) with hardware-specific register and peripheral code. `CONFIG_IDF_TARGET_ESP32P4` is referenced in `platformio.ini` but only virtual driver path is implemented.
|
Preliminary test works great on ESP32-P4-WIFI6-M. Continuing on to experiment to translate output to shift registers. |
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Refactor
Chores