Skip to content

esp32-p4 support - #54

Open
ewowi wants to merge 24 commits into
mainfrom
esp32-p4-support
Open

esp32-p4 support#54
ewowi wants to merge 24 commits into
mainfrom
esp32-p4-support

Conversation

@ewowi

@ewowi ewowi commented Apr 6, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • ESP32‑P4 support via PARLIO with initled()/showPixels() flow, show() alias, ColorArrangement wiring helper, and PARLIO_AUTO_OVERCLOCK option.
  • Bug Fixes

    • Fixed RGBCCT (warm‑white) mapping and improved behavior after topology/config changes.
  • Documentation

    • Expanded end‑user and developer docs: P4 behavior/constraints, integration guidance, compile‑time options, and supported targets.
  • Refactor

    • Safer buffer lifecycle and clearer hardware init/start/stop and reconfiguration sequencing.
  • Chores

    • Platform config tweak (commented dependency) and macros made statement‑safe.

@coderabbitai

coderabbitai Bot commented Apr 6, 2026

Copy link
Copy Markdown

Walkthrough

Adds 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

Cohort / File(s) Summary
Core driver & API
src/I2SClocklessLedDriver.h, src/I2SClocklessLedDriver.cpp
Driver API and lifecycle refactor: new hwInit/hwStart/hwStop split, init flags, show() alias, canonical initled() using channels+offsets, updated updateDriver() signature, and new helpers applyConfiguration(), initBuffers(), deleteBuffers(); teardown reorder in deleteDriver().
ESP32 D0/S3 I2S impl
src/esp32-d0s3_i2s_impl.h
New out‑of‑class ESP32/ESP32‑S3 implementation: DMA descriptor allocation, ISR handlers, ping‑pong refill, transposeColorChannel, loadAndTranspose, and hwStart/hwStop/i2sReset logic.
ESP32 P4 PARLIO impl
src/esp32-p4_parlio_impl.h
New PARLIO backend (guarded by CONFIG_IDF_TARGET_ESP32P4/HAS_PARLIO_DRIVER): waveform LUT, per‑width packers, lazy PARLIO init, buffer packing/transposition, chunked non‑blocking transmit queuing, and P4 hwStart/hwStop/loadAndTranspose implementations.
Channel layout helper
src/colorarrangement.h
Added ColorArrangement enum and applyColorArrangement() to derive channelsPerLight and per‑channel offsets used by canonical initled() and validation helpers.
Buffer & mapping plumbing
src/I2SClocklessLedDriver.*, src/esp32-d0s3_i2s_impl.h, src/esp32-p4_parlio_impl.h
Unified buffer lifecycle and mapping: dmaBuffersTampon renamed/used as transferBuffers, rgbwBufferMapping() integration, and centralized error/init handling across targets.
Helper macros
src/helper.h
Wrapped RUN_SKETCH_FOR and RUN_SKETCH_N_TIMES macros in do { ... } while (0) for single‑statement semantics.
Build config
platformio.ini
Commented out lib_deps FastLED entry (stops automatic PlatformIO fetch).
Docs — developer & end‑user
docs/developer/developer.md, docs/developer/workinprogress.md, docs/developer/standardsandguidelines.md, docs/enduser/enduser.md, docs/index.md, CLAUDE.md
Documented ESP32‑P4 PARLIO constraints and behavior (no I2S/DMA, no FULL_DMA_BUFFER/LOOP/showPixelsFromBuffer), added PARLIO_AUTO_OVERCLOCK, migrated guidance to hwInit/hwStart/hwStop terminology, updated semaphore/ISR notes, and added standards/WIP docs.
Misc — small edits
docs/*
Trailing newline, rename docs/migration entries (hardwareSpriteHardwareSprite), and various documentation clarifications and renames.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 PARLIO hums on P4 so bright,
Ping‑pong buffers dance by night,
Channels map and whites slip through,
Macros snug, impls split anew—
A rabbit hops and gives a byte!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'esp32-p4 support' is directly related to the main objective of this PR, which is to add ESP32-P4 platform support as documented across multiple files.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch esp32-p4-support

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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: Reconsider IRAM_ATTR on this function.

show_parlio_p4 is 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_ATTR is 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

📥 Commits

Reviewing files that changed from the base of the PR and between a736f5e and a205786.

📒 Files selected for processing (8)
  • CLAUDE.md
  • docs/developer/developer.md
  • docs/enduser/enduser.md
  • docs/index.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/parlio_p4.cpp
  • src/parlio_p4.h

Comment thread src/I2SClocklessLedDriver.cpp Outdated
Comment thread src/I2SClocklessLedDriver.h Outdated
Comment thread src/I2SClocklessLedDriver.h
Comment thread src/parlio_p4.cpp Outdated
Comment thread src/parlio_p4.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/helper.h (1)

13-23: ⚠️ Potential issue | 🔴 Critical

Restore the missing { in RUN_SKETCH_FOR.

Line 14 expands to do instead of do {, 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

📥 Commits

Reviewing files that changed from the base of the PR and between a205786 and 4e3befb.

📒 Files selected for processing (6)
  • docs/developer/developer.md
  • platformio.ini
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/helper.h
  • src/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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/I2SClocklessLedDriver.h (1)

1529-1569: Duplicated buffer size constant between files.

P4_BUF_BYTES here (line 1534) and REPACKED_BUFFER_BYTES in parlio_p4.cpp (line 340) both compute 1024 × 5 × 32 × 16 / 8 = 327,680. If one is updated without the other, the capacity check in show_parlio_p4() will silently skip frames when the allocation is actually sufficient (or vice versa).

Consider defining a single shared constant in parlio_p4.h that 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 outputs or max_leds changes, 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:

  1. Documenting this in the public API (show_parlio_p4 header comment or showPixels() docs)
  2. 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() and delayMicroseconds() 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.h

This 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4e3befb and 1dcb1b9.

📒 Files selected for processing (4)
  • docs/developer/developer.md
  • src/I2SClocklessLedDriver.h
  • src/helper.h
  • src/parlio_p4.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/helper.h

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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() sets initErrorOccurred but doesn't clear initSuccess — stale success state persists.

When setBrightness() fails to allocate a LUT (e.g., redMap), it sets initErrorOccurred = true and returns early. However, if initSuccess was already set to true from a prior successful initLedImpl(), it remains true.

This creates a window where showPixelsImpl() proceeds (because it only checks initSuccess, not initErrorOccurred) but LUT pointers may be null or partially allocated, leading to the crash risk in rgbwBufferMapping.

🐛 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 | 🟠 Major

LUT null-pointer dereference risk remains — verify initSuccess gating is sufficient.

The past review flagged that whiteMap, white2Map, redMap, greenMap, and blueMap are 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() checking initSuccess before calling show_parlio_p4(). However, per context snippet 2 (src/I2SClocklessLedDriver.h:1181-1202), if setBrightness() is called standalone after a successful initLedImpl() and fails to allocate LUTs, it sets initErrorOccurred = true but does not clear initSuccess. The next showPixelsImpl() call will proceed because initSuccess remains true.

🛡️ Suggested defensive fix in `show_parlio_p4` or `rgbwBufferMapping`

Option 1: Guard in show_parlio_p4 before calling create_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 initSuccess in setBrightness() on allocation failure (in I2SClocklessLedDriver.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_CHECK will abort on PARLIO API failures — consider graceful error handling.

Using ESP_ERROR_CHECK() on parlio_tx_unit_wait_all_done, parlio_tx_unit_disable, parlio_del_tx_unit, parlio_new_tx_unit, and parlio_tx_unit_enable will call abort() 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_cache is lazily initialized on first use with a static waveform_cache_initialized flag. If two tasks call show_parlio_p4 concurrently during startup, there's a potential race condition. However, this is likely acceptable given:

  1. LED driver calls are typically serialized by application code.
  2. The cache is idempotent (same values written on re-initialization).

Consider adding a brief comment noting this assumption, or use std::call_once if 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1dcb1b9 and 638c507.

📒 Files selected for processing (5)
  • docs/developer/standardsandguidelines.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/parlio_p4.cpp
  • src/parlio_p4.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/parlio_p4.h

Comment thread docs/developer/standardsandguidelines.md Outdated
Comment thread docs/developer/standardsandguidelines.md
Comment thread docs/developer/standardsandguidelines.md Outdated
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).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 text or plaintext as 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 dereferences driver->redMap, driver->greenMap, and driver->blueMap unconditionally (lines 200-202). If setBrightness() failed mid-allocation, these could be null.

The initSuccess check in showPixelsImpl() 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 if bytes_per_pixel is 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 > 0 guard is good, but if max_leds_per_chunk is 0, num_chunks becomes 1, and the loop at line 517 will attempt to transmit chunk_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 = 5 and SOC_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 in parlio_p4.cpp which 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 = true are good. However, if an allocation fails mid-way through setBrightness() (e.g., greenMap fails after redMap succeeds), the function returns early without filling the already-allocated LUTs. This could leave partially initialized LUTs if setBrightness() is called again later.

This is a minor edge case since initErrorOccurred will prevent further operation, but for robustness consider either:

  1. Allocating all LUTs first, then filling them, or
  2. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 638c507 and 92c6e2e.

⛔ Files ignored due to path filters (1)
  • docs/developer/PRflow.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • docs/developer/developer.md
  • docs/developer/standardsandguidelines.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/parlio_p4.cpp
✅ Files skipped from review due to trivial changes (1)
  • docs/developer/standardsandguidelines.md

Comment thread docs/developer/developer.md Outdated
┌────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────┐
  │                  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
@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

✅ Created PR with unit tests: #55

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

✅ Created PR with unit tests: #56

@MoonModules

Copy link
Copy Markdown

@coderabbitai resume and review

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

@MoonModules: Sure! Resuming automatic reviews and triggering a review of PR #54 now.


🧠 Learnings used
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} : Place nearly all `I2SClocklessLedDriver` class implementation, static ISR functions, and transpose functions in the `src/I2SClocklessLedDriver.h` header file. Use `src/I2SClocklessLedDriver.cpp` only for global variable definitions (`__NB_DMA_BUFFER`, `NUM_STRIPS`), `updateDriver()`, and `deleteDriver()` implementations.

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.
✅ Actions performed

Reviews resumed.


Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@MoonModules

Copy link
Copy Markdown

@coderabbitai, is your latest review of 1ec96ff in progress? (I don't see a pending coderabbit review)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 | 🔴 Critical

Critical: Static hwStop definition conflicts with P4 free function.

This static function definition conflicts with the extern declaration in parlio_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 adding IRAM_ATTR to transpose_32_slices for 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 whether IRAM_ATTR would 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

📥 Commits

Reviewing files that changed from the base of the PR and between 638c507 and 1ec96ff.

⛔ Files ignored due to path filters (1)
  • docs/developer/PRflow.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • docs/developer/developer.md
  • docs/developer/standardsandguidelines.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/colorarrangement.h
  • src/parlio_p4.cpp
  • src/parlio_p4.h

Comment thread src/I2SClocklessLedDriver.h
Comment thread src/I2SClocklessLedDriver.h Outdated
Comment thread src/I2SClocklessLedDriver.h Outdated
Comment thread src/parlio_p4.cpp Outdated
Comment thread src/parlio_p4.cpp Outdated
Comment thread src/parlio_p4.h Outdated
@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check PR `#54` reviews and review comments
gh pr view 54 --repo hpwit/I2SClocklessLedDriver --json reviews,commits,headRefOid,state

# List review comments on the PR
gh api repos/hpwit/I2SClocklessLedDriver/pulls/54/reviews --jq '.[] | {id: .id, user: .user.login, state: .state, submitted_at: .submitted_at, body: .body[0:100]}'

Length of output: 77318


@MoonModules: The review of commit 1ec96fffb58730708f93a94f5d237c99a7b98003 is already complete — it was posted today at 10:29 UTC (Review ID: PRR_kwDOEogyPM7y21Bi). It covered the incremental diff from 638c507 to 1ec96ff and posted 6 actionable comments with a CHANGES_REQUESTED state. You should see it on the PR now. The key findings include:

  • Critical (outside diff): static hwStop in I2SClocklessLedDriver.h conflicts with the extern declaration in parlio_p4.h — needs a #if !defined(CONFIG_IDF_TARGET_ESP32P4) guard.
  • Actionable: initTransferBuffers(this) call signature mismatch (member vs. free function from parlio_p4.h).
  • Actionable: static loadAndTranspose declaration in the header conflicts with the P4 free function — needs a platform guard.
  • Actionable: hwStop / hwStart / loadAndTranspose name conflicts between header statics and P4 free functions.
  • Nitpicks: IRAM_ATTR on transpose_32_slices, mixed "optimise/optimize" spelling in docs.

🧠 Learnings used
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/**/*.{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.

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} : Place nearly all `I2SClocklessLedDriver` class implementation, static ISR functions, and transpose functions in the `src/I2SClocklessLedDriver.h` header file. Use `src/I2SClocklessLedDriver.cpp` only for global variable definitions (`__NB_DMA_BUFFER`, `NUM_STRIPS`), `updateDriver()`, and `deleteDriver()` implementations.

@MoonModules

Copy link
Copy Markdown

@coderabbitai, regarding

Consider adding IRAM_ATTR to transpose_32_slices for ISR-path consistency.

The function is currently inline void transpose_32_slices(...) — no IRAM_ATTR.

The suggestion doesn't need to be applied for two reasons:

  1. inline and IRAM_ATTR are contradictory. IRAM_ATTR places a function's machine code in IRAM. An inline function has no out-of-line copy to place anywhere — it's expanded at each call site, landing wherever the
    caller lives. Adding IRAM_ATTR to an inline function has no effect.
  2. P4's loadAndTranspose is not an ISR path. On ESP32/S3, loadAndTranspose runs inside the DMA interrupt handler — that's the real reason the ESP32/S3 version carries IRAM_ATTR. On P4, loadAndTranspose is called
    synchronously from showPixelsImpl in task context. There is no ISR involved; the PARLIO driver is entirely synchronous. The "flash fetch latency on ISR-like execution paths" concern the reviewer raises doesn't
    apply here.

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

Tip

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

@MoonModules: You are correct on both counts — thank you for the clear explanation.

  1. inline + IRAM_ATTR incompatibility: An inline function has no out-of-line machine-code body to place in IRAM; it is expanded at each call site and lands wherever the caller resides. Applying IRAM_ATTR to an inline function is a no-op.
  2. P4 is not an ISR path: On ESP32/S3, loadAndTranspose is invoked from the DMA interrupt handler, which is the genuine reason IRAM_ATTR matters there. On P4, loadAndTranspose is called synchronously from showPixelsImpl in task context — no interrupt, no flash-fetch latency concern.

The suggestion was incorrect and should be disregarded. I've noted this for future reviews.


✏️ Learnings added
Learnt from: MoonModules
URL: https://github.com/hpwit/I2SClocklessLedDriver/pull/54

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.

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: 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} : Place nearly all `I2SClocklessLedDriver` class implementation, static ISR functions, and transpose functions in the `src/I2SClocklessLedDriver.h` header file. Use `src/I2SClocklessLedDriver.cpp` only for global variable definitions (`__NB_DMA_BUFFER`, `NUM_STRIPS`), `updateDriver()`, and `deleteDriver()` implementations.

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: 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} : Always make `NUM_STRIPS`, `__NB_DMA_BUFFER`, and `__delay` runtime variables (defined in `.cpp`, extern-declared in `.h`), not compile-time constants. Keep `updateDriver()` and `deleteDriver()` always available across all IDF versions.

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` (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 coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 #54 with 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 if setBrightness fails.

If setBrightness(255) at line 1455 fails (allocation error), it sets initErrorOccurred = true but initLedImpl continues to setPins, initTransferBuffers, etc. The final initSuccess check 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec96ff and 0c1cacd.

📒 Files selected for processing (4)
  • docs/developer/developer.md
  • docs/developer/standardsandguidelines.md
  • docs/developer/workinprogress.md
  • src/I2SClocklessLedDriver.h
✅ Files skipped from review due to trivial changes (2)
  • docs/developer/workinprogress.md
  • docs/developer/developer.md

Comment thread src/I2SClocklessLedDriver.h Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/parlio_p4.h (1)

43-48: Misleading documentation: hwInit() is void, not bool.

The comment states "Must only be called after hwInit() returns false" but hwInit() has no return value (it's void). 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0c1cacd and ec92a9e.

📒 Files selected for processing (5)
  • docs/developer/workinprogress.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/parlio_p4.cpp
  • src/parlio_p4.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/developer/workinprogress.md
  • src/parlio_p4.cpp

Comment thread src/I2SClocklessLedDriver.cpp Outdated
Comment thread src/I2SClocklessLedDriver.h Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 | 🔴 Critical

Validate channelsPerLight and channel indices at the API boundary.

This overload accepts arbitrary channelsPerLight, pR, pG, pB, pW, and pW2, but downstream code assumes 3 <= channelsPerLight <= 5 and writes into fixed-size buffers like mapped[5]. Passing channelsPerLight > 5 or an index >= channelsPerLight will 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 | 🔴 Critical

Propagate GDMA setup failures on the S3 path.

This block ignores the return values from gdma_new_*, gdma_connect(), gdma_apply_strategy(), and gdma_register_tx_event_callbacks(). Any one of those can fail and still leave initSuccess true, which turns the next hwStart() 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 | 🔴 Critical

Re-run hwInit() after deleteDriver() before rebuilding buffers.

deleteDriver() tears down the ESP32 interrupt handle and the S3 GDMA channel, but updateDriver() now goes straight from setPins() to initTransferBuffers(). After a successful reconfiguration, initSuccess can become true while intrHandle/dmaChan are still null, so the next showPixels() fails in hwStart().

🛠️ 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 | 🟠 Major

Remove the second sem release after hwStop(driver).

hwStop(driver) already gives driver->sem when driver->isWaiting is set. The extra give here bumps the semaphore twice for one frame completion, which can let a later WAIT take 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 | 🔴 Critical

Stop immediately when a transfer buffer allocation fails.

allocateDMABuffer() already returns NULL and sets initErrorOccurred, but this loop keeps going and Lines 777-778 dereference transferBuffers[i]->buffer unconditionally. 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 | 🟠 Major

Only release waitDisp when a waiter is actually pending.

These early exits now give waitDisp unconditionally. Because waitDisp is a counting semaphore, that leaves stale tokens behind; the next caller entering waitDisplay() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 35bd753 and 9c9c5ac.

📒 Files selected for processing (7)
  • docs/developer/standardsandguidelines.md
  • docs/developer/workinprogress.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/colorarrangement.h
  • src/esp32-d0s3_i2s_impl.h
  • src/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

Comment thread docs/developer/standardsandguidelines.md
Comment thread src/I2SClocklessLedDriver.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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c9c5ac and fa4a3e7.

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

Comment thread src/I2SClocklessLedDriver.h Outdated
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/I2SClocklessLedDriver.h (1)

812-819: ⚠️ Potential issue | 🟠 Major

Missing null check after allocateDMABuffer() calls.

The loop allocates DMA buffers but does not verify each allocation succeeded. If allocateDMABuffer() returns NULL due 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 / #else pattern at Line 62 leaves dead code in the #else branch. 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 #else branch 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

📥 Commits

Reviewing files that changed from the base of the PR and between fa4a3e7 and 8e6a9d8.

📒 Files selected for processing (2)
  • src/I2SClocklessLedDriver.cpp
  • src/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()

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 | 🟠 Major

Align .cpp contents with the repo’s header-first implementation rule.

deleteBuffers(), initBuffers(), and applyConfiguration() are now implemented in src/I2SClocklessLedDriver.cpp, but this repo’s rule for src/I2SClocklessLedDriver.{h,cpp} reserves .cpp for globals plus updateDriver()/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

📥 Commits

Reviewing files that changed from the base of the PR and between 8e6a9d8 and 581a504.

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

Comment thread src/I2SClocklessLedDriver.cpp
Comment thread src/I2SClocklessLedDriver.cpp
@ewowi

ewowi commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai, regarding

Align .cpp contents with the repo’s header-first implementation rule.

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

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown

Tip

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

@ewowi: You're right — that learning was incorrect and I apologize for the confusion. Standard C++ practice (declarations/class definitions in .h, implementations in .cpp) is what this repository follows. I'll remove the old learning and replace it with the correct one.


✏️ Learnings added
Learnt from: ewowi
URL: https://github.com/hpwit/I2SClocklessLedDriver/pull/54

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.

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 removed
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} : Place nearly all `I2SClocklessLedDriver` class implementation, static ISR functions, and transpose functions in the `src/I2SClocklessLedDriver.h` header file. Use `src/I2SClocklessLedDriver.cpp` only for global variable definitions (`__NB_DMA_BUFFER`, `NUM_STRIPS`), `updateDriver()`, and `deleteDriver()` implementations.

🧠 Learnings used
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} : Place nearly all `I2SClocklessLedDriver` class implementation, static ISR functions, and transpose functions in the `src/I2SClocklessLedDriver.h` header file. Use `src/I2SClocklessLedDriver.cpp` only for global variable definitions (`__NB_DMA_BUFFER`, `NUM_STRIPS`), `updateDriver()`, and `deleteDriver()` implementations.

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: CR
Repo: hpwit/I2SClocklessLedDriver PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T10:43:43.237Z
Learning: Applies to src/**/*.{h,cpp} : Reserve IDF version checks (`ESP_IDF_VERSION >= 5.5.0`) only for genuine API differences: header locations (`<esp_private/gpio.h>`, `<esp_private/periph_ctrl.h>` vs `<driver/gpio.h>`, `<driver/periph_ctrl.h>`), GPIO mux functions (`gpio_iomux_output()` vs `gpio_iomux_out()`), and GDMA channel allocation (`gdma_new_ahb_channel()` vs `gdma_new_channel()` with `isr_cache_safe` parameter).

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: 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: 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.

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/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`.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Potential memory leak on retry after partial setBrightness() failure.

If setBrightness() fails mid-way (e.g., greenMap allocation fails after redMap succeeds), initErrorOccurred is set but the successfully allocated maps are not freed. When initled() is retried, line 1367 resets initErrorOccurred but 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 until deleteDriver() 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 | 🟠 Major

Duplicate semaphore release after hwStop() on ESP32.

hwStop(driver) at line 241 already releases driver->sem when driver->isWaiting is true (see lines 194-196). The subsequent block at lines 242-246 releases sem again 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 text or plaintext.

📝 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8e6a9d8 and a2901a5.

📒 Files selected for processing (6)
  • docs/developer/developer.md
  • docs/developer/workinprogress.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/colorarrangement.h
  • src/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

Comment thread src/I2SClocklessLedDriver.h

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 | 🔴 Critical

Fix the undefined W identifier in the FULL_DMA white-extraction helpers.

Both overloads use W instead of the local white variable, so this path does not compile when FULL_DMA_BUFFER is 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 | 🟡 Minor

Remove the second sem give after hwStop(driver).

hwStop() already releases driver->sem when driver->isWaiting is 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 | 🟠 Major

Release waitDisp when 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 in waitDisplay() can stay stuck even though isDisplaying is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8e6a9d8 and a2901a5.

📒 Files selected for processing (6)
  • docs/developer/developer.md
  • docs/developer/workinprogress.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/colorarrangement.h
  • src/esp32-d0s3_i2s_impl.h
✅ Files skipped from review due to trivial changes (2)
  • src/colorarrangement.h
  • docs/developer/developer.md

Comment thread docs/developer/workinprogress.md Outdated
Comment thread src/I2SClocklessLedDriver.cpp
Comment thread src/I2SClocklessLedDriver.cpp
Comment thread src/I2SClocklessLedDriver.h Outdated
Comment thread src/I2SClocklessLedDriver.h

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 loadAndTranspose is called from the DMA ISR), but on ESP32-P4, loadAndTranspose is called synchronously from task context and does not have ISR-safety concerns.

Consider adding a note distinguishing the platforms:

  • ESP32/S3: loadAndTranspose, hwStop, interruptHandler are 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, loadAndTranspose is called synchronously from showPixelsImpl in 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8e6a9d8 and e41f89b.

📒 Files selected for processing (7)
  • docs/developer/developer.md
  • docs/developer/standardsandguidelines.md
  • docs/developer/workinprogress.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/colorarrangement.h
  • src/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

Comment on lines +33 to 37
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
}

@coderabbitai coderabbitai Bot Apr 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@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?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 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.

Comment thread src/I2SClocklessLedDriver.cpp
Comment thread src/I2SClocklessLedDriver.cpp
@ewowi

ewowi commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai resume and review

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown

@ewowi: Sure! Resuming automatic reviews and triggering a review of PR #54 now.


🧠 Learnings used
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: 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-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/**/*.{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.
✅ Actions performed

Reviews resumed.


Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ewowi

ewowi commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai are you currently reviewing latest commit?

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest commit and review status for PR `#54`
gh pr view 54 --repo hpwit/I2SClocklessLedDriver --json headRefOid,commits | jq '{headRefOid, latestCommit: .commits[-1]}'

# List all reviews and their timestamps
gh api repos/hpwit/I2SClocklessLedDriver/pulls/54/reviews --jq '.[] | {id: .id, user: .user.login, state: .state, submitted_at: .submitted_at, commit_id: .commit_id, body_preview: .body[0:80]}'

Length of output: 7277


@ewowi: The latest commit (23aa6a5506 — "🐰 fixes", pushed today at 07:58 UTC) has not been reviewed yet. The most recent completed review covered commit 7402d8a (April 9th). Since you just triggered @coderabbitai resume and review, the review of 23aa6a5506 is now queued and should be posted shortly.


🧠 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: 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.

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} : Reserve IDF version checks (`ESP_IDF_VERSION >= 5.5.0`) only for genuine API differences: header locations (`<esp_private/gpio.h>`, `<esp_private/periph_ctrl.h>` vs `<driver/gpio.h>`, `<driver/periph_ctrl.h>`), GPIO mux functions (`gpio_iomux_output()` vs `gpio_iomux_out()`), and GDMA channel allocation (`gdma_new_ahb_channel()` vs `gdma_new_channel()` with `isr_cache_safe` parameter).

Learnt from: ewowi
Repo: hpwit/I2SClocklessLedDriver PR: 51
File: src/main.cpp:1-2
Timestamp: 2026-04-05T18:39:10.248Z
Learning: In hpwit/I2SClocklessLedDriver, `src/main.cpp` is the standalone demo entrypoint guarded by `#ifdef PLATFORM_VERSION`. It is safely excluded from library consumers by two independent mechanisms: (1) `library.json` `build.srcFilter` explicitly excludes `main.cpp` with `"-<main.cpp>"`, and (2) `PLATFORM_VERSION` is only defined within the PlatformIO dev environments in `platformio.ini` and is never set for library consumers. No rename or additional guard is needed.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ewowi

ewowi commented Apr 11, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai. you said you where reviewing latest commit but I don't see anything coming so
please review 23aa6a5

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown

@ewowi: Apologies for the delay! I'll trigger a review of PR #54 now, which will cover commit 23aa6a5506faa7c88ad37b38832bf265058df9bc.


🧠 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: 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.
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 | 🔴 Critical

Don’t advertise 20 physical outputs on a 16-lane data path.

MAX_PINS now lets initled() accept up to 20 strips, but the physical driver path is still 16-wide: Lines::bytes is 16 bytes, signalsID only has 16 entries, and transposeColorChannel() in src/esp32-d0s3_i2s_impl.h only 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 | 🔴 Critical

Reject invalid channel layouts before persisting them.

The ColorArrangement overloads always pass a legal (channelsPerLight, offsets...) tuple, but this public overload accepts arbitrary values and forwards them straight into applyConfiguration(). Invalid or duplicate offsets can then index past the active channel range in rgbwBufferMapping() and the transpose path (for example the fixed mapped[5] buffer in src/esp32-d0s3_i2s_impl.h). Please add a shared validator here and reuse it from updateDriver() 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 | 🔴 Critical

P4 builds without PARLIO still reference methods that have no definition.

esp32-p4_parlio_impl.h is only included when HAS_PARLIO_DRIVER == 1, but the P4 show path and the loadAndTranspose()/hwStart()/hwStop() declarations remain visible even when HAS_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 with HAS_PARLIO_DRIVER too, 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 | 🟠 Major

Roll 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 the HAS_PARLIO_DRIVER == 0 P4 branch leaves target/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 | 🟡 Minor

Update 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-op hwInit() on P4, prepares p4Config in initBuffers(), and creates/enables the TX unit lazily in ensureParlioTxUnitInitialized() on the first showPixels() 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

📥 Commits

Reviewing files that changed from the base of the PR and between a736f5e and 23aa6a5.

⛔ Files ignored due to path filters (1)
  • docs/developer/PRflow.png is excluded by !**/*.png
📒 Files selected for processing (13)
  • CLAUDE.md
  • docs/developer/developer.md
  • docs/developer/standardsandguidelines.md
  • docs/developer/workinprogress.md
  • docs/enduser/enduser.md
  • docs/index.md
  • platformio.ini
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/colorarrangement.h
  • src/esp32-d0s3_i2s_impl.h
  • src/esp32-p4_parlio_impl.h
  • src/helper.h

Comment thread docs/developer/standardsandguidelines.md Outdated
Comment thread src/colorarrangement.h
Comment thread src/I2SClocklessLedDriver.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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/developer/standardsandguidelines.md (1)

75-75: ⚠️ Potential issue | 🟡 Minor

Use consistent product naming (“CodeRabbit”).

Line 75 still uses Coderabbit; elsewhere the doc uses CodeRabbit. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 23aa6a5 and ed5d315.

📒 Files selected for processing (5)
  • docs/developer/standardsandguidelines.md
  • docs/developer/workinprogress.md
  • src/I2SClocklessLedDriver.cpp
  • src/I2SClocklessLedDriver.h
  • src/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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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 overlay

Based 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.

Suggested change
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.

Comment thread src/I2SClocklessLedDriver.h Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between ed5d315 and 5681967.

📒 Files selected for processing (8)
  • CLAUDE.md
  • docs/developer/standardsandguidelines.md
  • docs/developer/workinprogress.md
  • docs/enduser/enduser.md
  • platformio.ini
  • src/I2SClocklessLedDriver.h
  • src/esp32-d0s3_i2s_impl.h
  • src/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

Comment thread docs/enduser/enduser.md
Comment on lines +334 to +349
### 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) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +1316 to +1344
#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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1433 to +1482
/** 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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

@ewowi

ewowi commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai, I am about to merge this PR. Please produce three outputs:

  1. PR review — in-depth review of all commits: a concise summary of what changed
    and why, a merge recommendation, and a prioritised list of follow-up actions.
    For the most urgent items (blockers or high-risk changes), include a ready-to-paste
    prompt that a AI Code agent can execute immediately before merge.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest reviews and comments
gh api repos/hpwit/I2SClocklessLedDriver/pulls/54/reviews --jq '.[] | {id: .id, user: .user.login, state: .state, submitted_at: .submitted_at, commit_id: .commit_id, body_preview: .body[0:200]}'

echo "=== LATEST COMMITS ==="
gh pr view 54 --repo hpwit/I2SClocklessLedDriver --json commits --jq '.commits[] | {oid: .oid, messageHeadline: .messageHeadline, committedDate: .committedDate}'

echo "=== OPEN REVIEW COMMENTS ==="
gh api repos/hpwit/I2SClocklessLedDriver/pulls/54/comments --jq '.[] | {id: .id, user: .user.login, path: .path, line: .line, body: .body[0:300], created_at: .created_at}'

Length of output: 26320


🏁 Script executed:

#!/bin/bash
echo "=== LATEST PR STATE ==="
gh pr view 54 --repo hpwit/I2SClocklessLedDriver --json headRefOid,state,mergeable

echo "=== HEAD COMMIT ==="
gh pr view 54 --repo hpwit/I2SClocklessLedDriver --json commits --jq '.commits[-3:]'

echo "=== Latest review on most recent commit ==="
gh api repos/hpwit/I2SClocklessLedDriver/pulls/54/reviews --jq 'sort_by(.submitted_at) | last | {id:.id, state:.state, submitted_at:.submitted_at, commit_id:.commit_id, body_preview:.body[0:500]}'

echo "=== Check for open/unresolved review comment threads ==="
gh api repos/hpwit/I2SClocklessLedDriver/pulls/54/comments --jq '[.[] | select(.user.login == "coderabbitai[bot]")] | length'

echo "=== Check hwStart/hwStop/loadAndTranspose platform guards in header ==="
rg -n "hwStop|hwStart|loadAndTranspose" src/I2SClocklessLedDriver.h | head -40

echo "=== Check semaphore release in esp32-p4_parlio_impl.h ==="
rg -n "xSemaphoreGive\|sem\|waitDisp\|isDisplaying" src/esp32-p4_parlio_impl.h | head -30

echo "=== Check initTransferBuffers call in header ==="
rg -n "initTransferBuffers" src/I2SClocklessLedDriver.h | head -10

echo "=== Check PARLIO includes ==="
rg -n "parlio_tx" src/I2SClocklessLedDriver.h | head -10
rg -n "parlio_tx" src/I2SClocklessLedDriver.cpp | head -10

Length of output: 6275


🏁 Script executed:

#!/bin/bash
echo "=== Lines 240-270 of I2SClocklessLedDriver.h (static loadAndTranspose guard?) ==="
sed -n '240,270p' src/I2SClocklessLedDriver.h

echo ""
echo "=== Lines 850-880 of I2SClocklessLedDriver.h (hwStart/hwStop context) ==="
sed -n '850,880p' src/I2SClocklessLedDriver.h

echo ""
echo "=== Lines 1280-1360 of I2SClocklessLedDriver.h (showPixelsImpl P4 path) ==="
sed -n '1280,1360p' src/I2SClocklessLedDriver.h

echo ""
echo "=== Lines 1540-1610 of I2SClocklessLedDriver.h (hwStart/hwStop/loadAndTranspose decls) ==="
sed -n '1540,1610p' src/I2SClocklessLedDriver.h

echo ""
echo "=== Check esp32-p4_parlio_impl.h for hwStop and semaphore/sem ==="
cat src/esp32-p4_parlio_impl.h | grep -n "hwStop\|xSemaphore\|sem\b\|waitDisp\|isDisplaying" | head -30

echo ""
echo "=== Check i2s_impl.h exists ==="
ls src/

echo ""
echo "=== Check for the duplicate semaphore release that was flagged in 7402d8a ==="
rg -n "xSemaphoreGive" src/esp32-d0s3_i2s_impl.h | head -20

Length of output: 7171


@ewowi: Here is the pre-merge review you requested. Note that your message was cut off after output #1 — I've provided the full review for that output below. If you intended to request additional outputs (e.g. a test plan or changelog), please let me know.


PR #54 Pre-Merge Review — ESP32-P4 Support

1. Summary of What Changed and Why

This PR adds full, hardware-backed ESP32-P4 support via the PARLIO TX peripheral, replacing the previous placeholder virtual-driver path. It also performs a structured, 9-phase architecture refactoring to give ESP32/ESP32-S3 and ESP32-P4 a shared, hardware-neutral call chain.

Architecture refactoring (Phases 1–9)

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 first showPixels() call
  • Chunked transmit for frames larger than 65 535 bytes (PARLIO DMA limit)
  • Synchronous hwStop() — waits for all chunks via parlio_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 using channelsPerLight + offset parameters
  • New show() alias for showPixels()
  • New validateChannelLayout() validates offset parameters before any state mutation
  • updateDriver() signature updated to match new channel-offset model
  • #error guard that catches FULL_DMA_BUFFER + CONFIG_IDF_TARGET_ESP32P4 at compile time

Final-commit bug fixes (ed5d315)

  • colorarrangement.h: pre-switch default initialisation prevents UB on unrecognised enum values
  • initBuffers() partial-allocation path now calls deleteBuffers() and restores initErrorOccurred = true to prevent heap leaks
  • Strip cap: numStrips > 16 rejected at runtime on ESP32/ESP32-S3 (buffer overflow prevention)
  • isDisplaying = false set on all early-exit paths in showPixelsImpl()
  • loadAndTranspose() returns bool; 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 loadAndTranspose conflict → guarded with #ifndef CONFIG_IDF_TARGET_ESP32P4
  • driver/parlio_tx.h missing include → now present
  • initTransferBuffers(this) call-signature mismatch → removed
  • loadAndTranspose redefinition / hwStop redefinition → resolved via class-method model + platform guards
  • isDisplaying leak 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.

@Flavourdynamics

Copy link
Copy Markdown

Preliminary test works great on ESP32-P4-WIFI6-M. Continuing on to experiment to translate output to shift registers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants