Skip to content

Adding ESPNOW connection to sync multiple hardware with sound reactive.#5637

Draft
Gallosauro wants to merge 6 commits into
wled:mainfrom
Gallosauro:main
Draft

Adding ESPNOW connection to sync multiple hardware with sound reactive.#5637
Gallosauro wants to merge 6 commits into
wled:mainfrom
Gallosauro:main

Conversation

@Gallosauro
Copy link
Copy Markdown

@Gallosauro Gallosauro commented May 21, 2026

Hello, first of all, I would like to thank everyone for the Wled. I have been using it for many years, and it is simply amazing. With the help of the AI I made this change that adds an option to the menu to connect multiple sound reactive devices with ESP now instead of UDP since I was having problems with the connection.
I built a custom-made hardware with an ESP32 Ue , PCM1808 and a Bluetooth receiver. The file compiles, and it works nicely so far. On the INFO menu, also display the frame loss and the sync status.

Recommended setup:

Master on send, Slave on receive, ESPNOW, and in case of different pinout : CheckboxSettingBrightness✅Color✅Effects✅Palette / Effect arguments✅Main segment only✅Segment options / Segment bounds / Segment data❌ untickedApply incoming sync✅

Known issue:

Every ESP32 has one radio. That radio can only be on one 2.4 GHz channel at a time. In a normal single-AP Wi-Fi setup, every WLED board associates to the same AP, so every radio ends up on the same channel — ESP-NOW broadcasts reach everyone. In a mesh, each mesh node (satellite) operates on its own channel, and your WLED boards roam to whichever node is closest. So board #1 ends up on channel 1, board #2 on channel 6, board #3 on channel 11 — and they literally can't hear each other's ESP-NOW packets even though they're on the same Wi-Fi network. WLED's state sync still works (it goes through the AP and onto the IP network), but ESP-NOW is peer-to-peer and channel-pinned.
Easiest fix: Lock the mesh to a single 2.4 GHz channel.

Audio Reactive ESP-NOW Patch — Complete Edit Recipe

This is the full explanation of the code from the AI:

Audio Reactive ESP-NOW Patch — Complete Edit Recipe
Canonical single-doc version. Applies to a clean copy of WLED v16.0.0's usermods/audioreactive/audio_reactive.cpp (2307 lines). Eleven edits total — overlapping ones from the original two-doc workflow are now merged.
Drop espnow_audio_sync.h into the same folder before starting.
How to apply each edit: in VSCode, Ctrl+F → click the small arrow to open Find & Replace → paste the FIND block → paste the REPLACE block → hit Replace once. Each FIND block is unique in the file, so there's no ambiguity.


Edit 0 — include the transport module
What it does: makes the ESP-NOW transport API available to the rest of the file.
FIND: (in the top include block, around line 12 — already done in your current file if you see this line)
#include <esp_timer.h>
REPLACE WITH:
#include <esp_timer.h>

#include "espnow_audio_sync.h"


Edit 1 — class members for the two GUI settings
What it does: adds two uint8_t members that hold the user's choices: which transport (UDP or ESP-NOW) and which channel (auto or forced 1–13).
FIND:
uint16_t audioSyncPort= 11988;// default port for UDP sound sync
REPLACE WITH:
uint16_t audioSyncPort= 11988;// default port for UDP sound sync
uint8_t audioSyncTransport = 0; // NEW: 0 = UDP (stock), 1 = ESP-NOW (low-latency)
uint8_t espnowChannel = 0; // NEW: 0 = auto (follow Wi-Fi), 1-13 = force channel


Edit 2 — config-key string declarations
What it does: declares the names that will appear as JSON keys in cfg.json under the sync section.
FIND:
static const char UDP_SYNC_HEADER[];
static const char UDP_SYNC_HEADER_v1[];
REPLACE WITH:
static const char UDP_SYNC_HEADER[];
static const char UDP_SYNC_HEADER_v1[];
static const char _syncTransport[];
static const char _espnowChannel[];


Edit 3 — config-key string definitions
What it does: assigns the actual JSON-key strings ("transport" and "espnow-channel") to the labels declared above. These are stored in PROGMEM to save RAM.
FIND:
const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature
REPLACE WITH:
const char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = "00001"; // old sync header version - need to add backwards-compatibility feature
const char AudioReactive::_syncTransport[] PROGMEM = "transport";
const char AudioReactive::_espnowChannel[] PROGMEM = "espnow-channel";


Edit 4 — initialise ESP-NOW when Wi-Fi connects
What it does: every time WLED's connected() fires (boot or Wi-Fi reconnect), this brings up the ESP-NOW transport. The configured channel overrides the auto-detected one if the user has chosen a fixed channel in the GUI. espnowAudioSync::begin() auto-detects whether to run hosted (alongside WLED's own ESP-NOW for WiZmote/state-sync) or standalone (we own the radio).
FIND:
void connected() override
{
if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection
udpSyncConnected = false;
fftUdp.stop();
}

  if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) {
  #ifdef ARDUINO_ARCH_ESP32
    udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort);
  #else
    udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort);
  #endif
  }
}

REPLACE WITH:
void connected() override
{
if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection
udpSyncConnected = false;
fftUdp.stop();
}

  if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) {
  #ifdef ARDUINO_ARCH_ESP32
    udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort);
  #else
    udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort);
  #endif
  }
  // NEW: bring up ESP-NOW transport (auto-detects hosted vs standalone)
  // espnowChannel: 0 = auto (follow Wi-Fi), 1-13 = force channel
  const uint8_t syncChannel = (espnowChannel >= 1 && espnowChannel <= 13)
                              ? espnowChannel
                              : WiFi.channel();
  const bool    amSender    = (audioSyncEnabled & 0x01) != 0;
  if (!espnowAudioSync::begin(syncChannel, amSender)) {
    DEBUG_PRINTLN(F("[ENaudio] espnowAudioSync::begin() failed - falling back to UDP."));
  }
}

Edit 5 — send over ESP-NOW (master)
What it does: in transmitAudioData(), if the user has selected the ESP-NOW transport, hand the packet to ESP-NOW. Otherwise keep doing the stock UDP multicast. Either path uses the same 44-byte V2 packet.
FIND:
if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error
fftUdp.write(reinterpret_cast<uint8_t *>(&transmitData), sizeof(transmitData));
fftUdp.endPacket();
}
REPLACE WITH:
if (audioSyncTransport == 1) {
// NEW: ESP-NOW transport
espnowAudioSync::send(&transmitData, sizeof(transmitData));
} else {
// UDP transport (stock behaviour)
if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error
fftUdp.write(reinterpret_cast<uint8_t *>(&transmitData), sizeof(transmitData));
fftUdp.endPacket();
}
}


Edit 6 — receive over ESP-NOW (slave)
What it does: in receiveAudioData(), if the user has selected the ESP-NOW transport, poll the addon for the latest packet and decode it through the same decodeAudioData() the UDP path uses. Otherwise the function falls through to the stock UDP code unchanged.
FIND:
bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received.
{
if (!udpSyncConnected) return false;
bool haveFreshData = false;
REPLACE WITH:
bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received.
{
if (audioSyncTransport == 1) {
// NEW: ESP-NOW transport
audioSyncPacket enFrame;
uint32_t enDeadline = 0;
if (espnowAudioSync::poll(&enFrame, sizeof(enFrame), &enDeadline, /renderDelayUs=/15000)) {
decodeAudioData(sizeof(enFrame), reinterpret_cast<uint8_t*>(&enFrame));
receivedFormat = 2;
last_UDPTime = millis();
return true;
}
return false;
}
if (!udpSyncConnected) return false;
bool haveFreshData = false;


Edit 7 — persist the new settings (save side)
What it does: adds both new settings to the sync JSON object that WLED writes to cfg.json whenever the user saves.
FIND:
JsonObject sync = top.createNestedObject("sync");
sync["port"] = audioSyncPort;
sync["mode"] = audioSyncEnabled;
}
REPLACE WITH:
JsonObject sync = top.createNestedObject("sync");
sync["port"] = audioSyncPort;
sync["mode"] = audioSyncEnabled;
sync[FPSTR(_syncTransport)] = audioSyncTransport;
sync[FPSTR(_espnowChannel)] = espnowChannel;
}


Edit 8 — persist the new settings (load side)
What it does: reads the two new settings back from cfg.json on boot. The (uint8_t)0 is the default used if the field is missing (first boot after flashing the patch, before the user has saved anything).
FIND:
configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort);
configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled);
REPLACE WITH:
configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort);
configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled);
configComplete &= getJsonValue(top["sync"][FPSTR(_syncTransport)], audioSyncTransport, (uint8_t)0);
configComplete &= getJsonValue(top["sync"][FPSTR(_espnowChannel)], espnowChannel, (uint8_t)0);


Edit 9 — add the two dropdowns to the Sound Settings page
What it does: emits the JavaScript that builds the Audio Sync Transport and espnow-channel dropdowns in WLED's web UI under Config → Sound Settings. Both appear right after the existing Sync (Off/Send/Receive) dropdown.
FIND:
uiScript.print(F("addOption(dd,'Receive',2);"));
REPLACE WITH:
uiScript.print(F("addOption(dd,'Receive',2);"));
// NEW: Audio Sync Transport selector (UDP vs ESP-NOW)
uiScript.print(F("dd=addDropdown(ux,'sync:transport');"));
uiScript.print(F("addOption(dd,'UDP (default)',0);"));
uiScript.print(F("addOption(dd,'ESP-NOW (low-latency)',1);"));
// NEW: ESP-NOW channel selector (Auto + 1..13)
uiScript.print(F("dd=addDropdown(ux,'sync:espnow-channel');"));
uiScript.print(F("addOption(dd,'Auto (follow Wi-Fi)',0);"));
uiScript.print(F("addOption(dd,'1',1);"));
uiScript.print(F("addOption(dd,'2',2);"));
uiScript.print(F("addOption(dd,'3',3);"));
uiScript.print(F("addOption(dd,'4',4);"));
uiScript.print(F("addOption(dd,'5',5);"));
uiScript.print(F("addOption(dd,'6',6);"));
uiScript.print(F("addOption(dd,'7',7);"));
uiScript.print(F("addOption(dd,'8',8);"));
uiScript.print(F("addOption(dd,'9',9);"));
uiScript.print(F("addOption(dd,'10',10);"));
uiScript.print(F("addOption(dd,'11',11);"));
uiScript.print(F("addOption(dd,'12',12);"));
uiScript.print(F("addOption(dd,'13',13);"));


Edit 10 — show ESP-NOW stats on the Info pane
What it does: when the user has switched to the ESP-NOW transport, the WLED home page's Info pane shows live counters: how many frames have been received, the rolling frame-loss percentage, how many packets the master has queued for transmit, any TX errors, and which mode the addon is running in (hosted / standalone). Healthy slaves show RX climbing fast and Frame Loss under 1 %.
FIND:
if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) {
if (receivedFormat == 1) infoArr.add(F(" v1"));
if (receivedFormat == 2) infoArr.add(F(" v2"));
}
REPLACE WITH:
if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) {
if (receivedFormat == 1) infoArr.add(F(" v1"));
if (receivedFormat == 2) infoArr.add(F(" v2"));
}

    // NEW: ESP-NOW audio sync stats (only shown when transport is set to ESP-NOW)
    if (audioSyncTransport == 1) {
      auto s = espnowAudioSync::stats();

      infoArr = user.createNestedArray(F("ESP-NOW Audio RX"));
      infoArr.add(s.rx);
      infoArr.add(F(" frames"));

      infoArr = user.createNestedArray(F("ESP-NOW Frame Loss"));
      uint32_t denom = s.rx + s.gaps;
      if (denom > 0) {
        char buf[16];
        snprintf(buf, sizeof(buf), "%.2f %%", 100.0f * (float)s.gaps / (float)denom);
        infoArr.add(buf);
      } else {
        infoArr.add(F("--"));
      }

      infoArr = user.createNestedArray(F("ESP-NOW Audio TX"));
      infoArr.add(s.tx);
      infoArr.add(F(" packets"));

      if (s.tx_err > 0) {
        infoArr = user.createNestedArray(F("ESP-NOW TX Errors"));
        infoArr.add(s.tx_err);
      }

      infoArr = user.createNestedArray(F("ESP-NOW Mode"));
      infoArr.add(espnowAudioSync::isHosted() ? F("hosted (sharing WLED stack)") : F("standalone"));
    }

Edit 11 — coexistence hook with WLED's own ESP-NOW
What it does: WLED's core ESP-NOW dispatcher (in wled00/udp.cpp) calls UsermodManager::onEspNowMessage(...) on every incoming ESP-NOW packet before it does its own WiZmote / state-sync handling. By overriding this method on the AudioReactive class, we get first look at every packet. We claim audio-sync packets (their magic bytes match 0xA53E) and return true → WLED skips its own handling for that packet. For anything else (WiZmote button presses, ESP-NOW state sync, etc.) we return false and WLED proceeds normally. This is what lets the WiZmote remote and ESP-NOW state-sync features keep working alongside the audio sync. The whole thing is wrapped in #ifndef WLED_DISABLE_ESPNOW so the build still compiles if you ever disable ESP-NOW in build flags.
FIND: (this multi-line block is unique — copy it exactly, including the two blank lines between onStateChange and the addToConfig comment)
createAudioPalettes();
}
}

/*
 * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object.

REPLACE WITH:
createAudioPalettes();
}
}

#ifndef WLED_DISABLE_ESPNOW
// ESP-NOW audio-sync coexistence hook. WLED's core ESP-NOW dispatcher
// (espNowReceiveCB in wled00/udp.cpp) calls this on every received
// packet BEFORE its own WiZmote/state-sync handling. We grab audio-sync
// packets and return true; WLED then skips the rest of its dispatch.
bool onEspNowMessage(uint8_t* sender, uint8_t* data, uint8_t len) override {
return espnowAudioSync::handleIncomingPacket(data, len);
}
#endif

Summary by CodeRabbit

  • New Features

    • ESP-NOW added as an alternative transport for audioreactive sync
    • Transport selection exposed in UI/config; info page shows ESP-NOW RX/TX stats, loss, and TX errors
  • Improvements

    • Optimized FFT buffer allocation and cleanup for float and integer DSP paths
    • Improved UDP receive buffering, realtime-mode suspension logic, and ESP-NOW/Wi‑Fi coexistence for more reliable sync

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

Walkthrough

Adds an ESP-NOW transport for audioreactive sync, integrates transport selection and persistence in AudioReactive (transmit/receive/init/UI/info), refines UDP/realtime behavior, and updates FFT memory allocations and integer FFT scaling.

Changes

Audio Sync Transport & FFT Improvements

Layer / File(s) Summary
ESP-NOW Audio Sync Protocol
usermods/audioreactive/espnow_audio_sync.h
Defines the ESP-NOW wire protocol with ENPacket envelope (magic/version/sequence/timestamp), initialization for hosted vs. standalone modes, transmission with duplicate copies for loss resilience, reception with dedup/gap tracking, and diagnostics (isHosted(), stats()).
FFT Memory Management
usermods/audioreactive/audio_reactive.cpp
Replaces standard allocations with aligned heap_caps_aligned_calloc for float and integer FFT buffers/windows, adds allocation failure cleanup and frees temporary buffers via heap_caps_free, and scales integer FFT magnitude by 512.
Transport selection, config & UI wiring
usermods/audioreactive/audio_reactive.cpp
Adds audioSyncTransport member and _syncTransport config key; persists and reads sync[transport]; adds UI dropdown for UDP vs ESP-NOW and defines the static key constant.
AudioReactive Transport Integration & Hooks
usermods/audioreactive/audio_reactive.cpp
Branches transmitAudioData() and receiveAudioData() to route between UDP and espnowAudioSync APIs; initializes ESP-NOW in connected() when selected; adds onEspNowMessage() override to consume audio-sync ESP-NOW frames; surfaces ESP-NOW runtime stats in /json/info.
Realtime Mode & UDP Receive Refinements
usermods/audioreactive/audio_reactive.cpp
Refines realtime-mode suspension gating to require realtimeOverride == REALTIME_OVERRIDE_NONE with explicit modes and flushes UDP input on ESP32 when no packet is consumed; ensures sound processing is enabled on update begin.

Sequence Diagram(s)

sequenceDiagram
  participant Sender as AudioReactive(sender)
  participant espnowAudioSync
  participant Radio as ESPNowRadio
  participant Receiver as AudioReactive(receiver)
  Sender->>espnowAudioSync: send(44B payload) (stamp seq,timestamp)
  espnowAudioSync->>Radio: transmit ENPacket x2 (duplicate flag)
  Radio-->>Receiver: _onRecv callback with ENPacket bytes
  Receiver->>espnowAudioSync: handleIncomingPacket(data,len)
  espnowAudioSync->>espnowAudioSync: dedup/gap track & latch payload
  Receiver->>espnowAudioSync: poll(out, deadline_us)
  espnowAudioSync-->>Receiver: payload + render deadline
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • wled/WLED#4859: Changes to WLED usermod ESP-NOW/UDP packet hooks intersect with the onEspNowMessage dispatch integration in this PR.

Suggested labels

external

Suggested reviewers

  • DedeHai
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding ESP-NOW as an alternate transport mechanism for the audio-reactive sound-sync feature to enable multi-device synchronization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
usermods/audioreactive/audio_reactive.cpp (1)

321-344: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Memory leak if dsps_fft2r_init_sc16() fails.

If dsps_fft2r_init_sc16(NULL, samplesFFT) at line 333 returns an error, windowFloat has been allocated but is not freed before returning. The cleanup at lines 327-331 handles allocation failures, but line 333's failure path just returns without freeing windowFloat.

Proposed fix
-  if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables
+  if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) { // initialize FFT tables
+    heap_caps_free(windowFloat);
+    heap_caps_free(windowFFT); windowFFT = nullptr;
+    heap_caps_free(valFFT); valFFT = nullptr;
+    return;
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/audioreactive/audio_reactive.cpp` around lines 321 - 344, If
dsps_fft2r_init_sc16(NULL, samplesFFT) returns an error we must free the
temporary allocations to avoid a leak: before returning from the failing
dsps_fft2r_init_sc16 call, call heap_caps_free on windowFloat, windowFFT and
valFFT if they are non-null (and set those pointers to nullptr) so all buffers
allocated earlier (windowFloat, windowFFT, valFFT) are cleaned up; ensure this
cleanup mirrors the allocation-failure block and then return.
🧹 Nitpick comments (3)
usermods/audioreactive/espnow_audio_sync.h (2)

1-34: 💤 Low value

Add AI attribution comments around this AI-generated code block.

Per coding guidelines, AI-generated code blocks should be wrapped with // AI: below section was generated by an AI ... // AI: end comments. The PR description indicates this file was AI-generated.

Proposed fix
+// AI: below section was generated by an AI
 // espnow_audio_sync.h
 //
 // Drop-in ESP-NOW transport for WLED audioreactive sync (v16.0.0 mainline).

And at the end of the file (after the closing brace of the namespace):

 } // namespace espnowAudioSync
+// AI: end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/audioreactive/espnow_audio_sync.h` around lines 1 - 34, The header
is AI-generated and needs explicit AI-attribution comment markers; add a leading
comment line "// AI: below section was generated by an AI" immediately before
the start of the AI-generated block (i.e., before the current top comment block
or whatever precedes the espnow_audio_sync.h content) and a trailing line "//
AI: end" immediately after the end of the generated code (after the closing
brace of the namespace used in this file), so that the entire generated region
(including the public API comments and any namespace declarations/functions like
begin, send, poll, handleIncomingPacket if present) is wrapped between those two
markers.

72-97: ⚖️ Poor tradeoff

Header with static state risks ODR violations if included from multiple translation units.

The comment on line 73 acknowledges this must be included from exactly one .cpp file. However, this is fragile - accidental inclusion elsewhere would silently create duplicate, disconnected state copies. Consider moving state to a separate .cpp file or using inline variables (C++17) to enforce single definition.

Additionally, stats counters (_stat_rx, _stat_dupes, etc.) are incremented inside handleIncomingPacket() which can be called from the ESP-NOW receive callback (ISR context). While diagnostic-only, the read-modify-write increments are technically racy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/audioreactive/espnow_audio_sync.h` around lines 72 - 97, The header
currently defines many file-scope static variables (_channel, _isSender,
_initialised, _hosted, _txSeq, _rxReady, _rxPayload, _rxMux, _stat_rx,
_stat_dupes, _stat_gaps, _stat_tx, _stat_tx_err, etc.), which risks ODR
violations if the header is ever included from multiple translation units; move
these state definitions into a single .cpp (e.g., espnow_audio_sync.cpp) and
leave only extern declarations in the header, or mark them as inline variables
(C++17+) in the header to guarantee one definition. Also, protect the counter
increments performed in handleIncomingPacket() (and any ESP-NOW callback) from
ISR/race issues by making stats atomic types (std::atomic<uint32_t>) or by
wrapping read-modify-write operations with the existing portMUX critical section
(portENTER_CRITICAL/portEXIT_CRITICAL) using _rxMux or a dedicated mutex. Ensure
you update declarations/uses of the variables and handlers accordingly so the
receive callback and handleIncomingPacket() reference the single shared
definitions.
usermods/audioreactive/audio_reactive.cpp (1)

1311-1328: ⚡ Quick win

Consider version-aware UDP buffer clearing.

Based on learnings, WiFiUDP::flush() is deprecated and ineffective for RX draining in arduino-esp32 v3.x (ESP-IDF 5.x). For compatibility across versions:

-      if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush();
+#if ESP_IDF_VERSION_MAJOR >= 5
+      if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.clear();
+#else
+      if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush();
+#endif

Based on learnings: WiFiUDP::flush() should not be used to clear/drain the UDP receive buffer on ESP-IDF 5.x; use WiFiUDP::clear() instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/audioreactive/audio_reactive.cpp` around lines 1311 - 1328, Replace
the direct call to fftUdp.flush() in the ARDUINO_ARCH_ESP32 branch with a
version-aware drain: call fftUdp.clear() when compiling against newer
arduino-esp32/ESP-IDF 5.x (or when a clear() symbol is available) and fall back
to fftUdp.flush() for older SDKs; update the conditional around the
invalid-packet check that currently uses fftUdp.parsePacket() and fftUdp.flush()
so it uses fftUdp.clear() when supported (keep the same size checks involving
UDPSOUND_MAX_PACKET) and ensure compile-time detection uses a macro such as
ESP_IDF_VERSION_MAJOR or a WIFI_UDP_HAS_CLEAR probe to pick the correct method.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@usermods/audioreactive/audio_reactive.cpp`:
- Around line 1536-1541: In connected() avoid calling espnowAudioSync::begin()
when audioSyncTransport==0 (UDP); change the logic so you only compute
syncChannel/amSender and call espnowAudioSync::begin(syncChannel, amSender) if
audioSyncTransport != 0 and audioSyncEnabled indicates sender/receiver as
before, otherwise skip ESP-NOW init and leave the DEBUG_PRINTLN fallback logic
for when ESP-NOW is selected but begin() fails; reference
espnowAudioSync::begin, audioSyncTransport, audioSyncEnabled and the connected()
function when making this conditional gate.

---

Outside diff comments:
In `@usermods/audioreactive/audio_reactive.cpp`:
- Around line 321-344: If dsps_fft2r_init_sc16(NULL, samplesFFT) returns an
error we must free the temporary allocations to avoid a leak: before returning
from the failing dsps_fft2r_init_sc16 call, call heap_caps_free on windowFloat,
windowFFT and valFFT if they are non-null (and set those pointers to nullptr) so
all buffers allocated earlier (windowFloat, windowFFT, valFFT) are cleaned up;
ensure this cleanup mirrors the allocation-failure block and then return.

---

Nitpick comments:
In `@usermods/audioreactive/audio_reactive.cpp`:
- Around line 1311-1328: Replace the direct call to fftUdp.flush() in the
ARDUINO_ARCH_ESP32 branch with a version-aware drain: call fftUdp.clear() when
compiling against newer arduino-esp32/ESP-IDF 5.x (or when a clear() symbol is
available) and fall back to fftUdp.flush() for older SDKs; update the
conditional around the invalid-packet check that currently uses
fftUdp.parsePacket() and fftUdp.flush() so it uses fftUdp.clear() when supported
(keep the same size checks involving UDPSOUND_MAX_PACKET) and ensure
compile-time detection uses a macro such as ESP_IDF_VERSION_MAJOR or a
WIFI_UDP_HAS_CLEAR probe to pick the correct method.

In `@usermods/audioreactive/espnow_audio_sync.h`:
- Around line 1-34: The header is AI-generated and needs explicit AI-attribution
comment markers; add a leading comment line "// AI: below section was generated
by an AI" immediately before the start of the AI-generated block (i.e., before
the current top comment block or whatever precedes the espnow_audio_sync.h
content) and a trailing line "// AI: end" immediately after the end of the
generated code (after the closing brace of the namespace used in this file), so
that the entire generated region (including the public API comments and any
namespace declarations/functions like begin, send, poll, handleIncomingPacket if
present) is wrapped between those two markers.
- Around line 72-97: The header currently defines many file-scope static
variables (_channel, _isSender, _initialised, _hosted, _txSeq, _rxReady,
_rxPayload, _rxMux, _stat_rx, _stat_dupes, _stat_gaps, _stat_tx, _stat_tx_err,
etc.), which risks ODR violations if the header is ever included from multiple
translation units; move these state definitions into a single .cpp (e.g.,
espnow_audio_sync.cpp) and leave only extern declarations in the header, or mark
them as inline variables (C++17+) in the header to guarantee one definition.
Also, protect the counter increments performed in handleIncomingPacket() (and
any ESP-NOW callback) from ISR/race issues by making stats atomic types
(std::atomic<uint32_t>) or by wrapping read-modify-write operations with the
existing portMUX critical section (portENTER_CRITICAL/portEXIT_CRITICAL) using
_rxMux or a dedicated mutex. Ensure you update declarations/uses of the
variables and handlers accordingly so the receive callback and
handleIncomingPacket() reference the single shared definitions.
🪄 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: ea437e91-8fcb-449d-978a-e33d3bbbafb4

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec2579 and c0dba84.

📒 Files selected for processing (2)
  • usermods/audioreactive/audio_reactive.cpp
  • usermods/audioreactive/espnow_audio_sync.h

Comment thread usermods/audioreactive/audio_reactive.cpp Outdated
@coderabbitai coderabbitai Bot added external Not part of WLED itself - an external plugin/remote etc. usermod usermod related labels May 21, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
usermods/audioreactive/espnow_audio_sync.h (1)

38-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard ESP-NOW header + fix one-shot init on reconnects.

  • usermods/audioreactive/audio_reactive.cpp includes usermods/audioreactive/espnow_audio_sync.h unconditionally, but espnow_audio_sync.h unconditionally includes ESP32/IDF headers (esp_now.h / esp_wifi.h / esp_idf_version.h)—so non-ESP32 builds can fail at compile time even though the begin() call site is inside #ifdef ARDUINO_ARCH_ESP32.
  • espnowAudioSync::begin() is one-shot (_initialised early return) while audio_reactive.cpp::connected() runs on every WiFi (re)connect, so STANDALONE mode can keep stale pinned channel/peer state after reconnect.
Possible shape of the fix
+#ifdef ARDUINO_ARCH_ESP32
 `#include` <Arduino.h>
 `#include` <WiFi.h>
 `#include` <esp_now.h>
 `#include` <esp_wifi.h>
 `#include` <esp_timer.h>
 `#include` <esp_idf_version.h>
 `#include` <string.h>
+
+namespace espnowAudioSync {
+  // current implementation
+}
+#else
+#include <Arduino.h>
+
+namespace espnowAudioSync {
+  struct Stats { uint32_t rx, dupes, gaps, tx, tx_err; };
+  inline bool begin(uint8_t, bool) { return false; }
+  inline bool send(const void*, size_t) { return false; }
+  inline bool poll(void*, size_t, uint32_t* = nullptr, uint32_t = 15000) { return false; }
+  inline bool handleIncomingPacket(const uint8_t*, int) { return false; }
+  inline bool isHosted() { return false; }
+  inline Stats stats() { return Stats{0, 0, 0, 0, 0}; }
+}
+#endif
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/audioreactive/espnow_audio_sync.h` around lines 38 - 43, Wrap the
ESP-IDF/ESP32-specific headers in espnow_audio_sync.h inside an `#ifdef`
ARDUINO_ARCH_ESP32 guard so non-ESP32 builds don't include
esp_now.h/esp_wifi.h/esp_idf_version.h, and modify espnowAudioSync::begin() so
it is idempotent on reconnects instead of a one-shot: remove the unconditional
early return on _initialised (or detect reconnects) and if already initialised
perform a safe reinit sequence (call esp_now_deinit() or clear peers and re-run
esp_now_init()/peer add/channel setup) before reconfiguring; this ensures
audio_reactive.cpp::connected() can call begin() on every WiFi (re)connect
(including STANDALONE mode) to refresh pinned channel/peer state while still
setting _initialised at the end.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@usermods/audioreactive/audio_reactive.cpp`:
- Around line 1541-1546: The ESP-NOW init failure logs a UDP fallback but never
actually flips the transport, so later send/receive still use ESP-NOW; in the
espnowAudioSync::begin() failure branch either set the transport back to UDP
(e.g. set audioSyncTransport = 0) or set a dedicated runtime-ready flag (e.g.
espnowReady = false) and update the later transport decision points to check
that flag (or call an espnowAudioSync::isReady()) before taking the ESP-NOW
branch; modify the code paths that use audioSyncTransport to gate on the
successful init flag (or the updated audioSyncTransport) so the module truly
falls back to UDP when begin() fails (refer to audioSyncTransport,
espnowAudioSync::begin, and the later send/receive transport branches).

In `@usermods/audioreactive/espnow_audio_sync.h`:
- Around line 97-98: The static flag _initialised makes begin() a no-op after
the first success so connected() can't refresh channel/peer state on reconnect;
change begin() (and any STANDALONE-mode init paths that check _initialised) to
detect channel or peer changes and reapply ESP-NOW configuration instead of
returning early, or add a clear/reset helper that forces reinitialisation and
re-registers the broadcast peer; specifically update the logic around
_initialised, begin(), and the reconnect hook connected() to either (1) compare
current Wi‑Fi channel/peer info and perform reconfiguration when different, or
(2) expose and call a reset/reinit function from connected() to unregister and
re-register peers and set the channel before reinitialising ESP-NOW.

---

Outside diff comments:
In `@usermods/audioreactive/espnow_audio_sync.h`:
- Around line 38-43: Wrap the ESP-IDF/ESP32-specific headers in
espnow_audio_sync.h inside an `#ifdef` ARDUINO_ARCH_ESP32 guard so non-ESP32
builds don't include esp_now.h/esp_wifi.h/esp_idf_version.h, and modify
espnowAudioSync::begin() so it is idempotent on reconnects instead of a
one-shot: remove the unconditional early return on _initialised (or detect
reconnects) and if already initialised perform a safe reinit sequence (call
esp_now_deinit() or clear peers and re-run esp_now_init()/peer add/channel
setup) before reconfiguring; this ensures audio_reactive.cpp::connected() can
call begin() on every WiFi (re)connect (including STANDALONE mode) to refresh
pinned channel/peer state while still setting _initialised at the end.
🪄 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: f8ddba42-2298-47e1-912a-9dd5dd7d3987

📥 Commits

Reviewing files that changed from the base of the PR and between c0dba84 and f39c92a.

📒 Files selected for processing (2)
  • usermods/audioreactive/audio_reactive.cpp
  • usermods/audioreactive/espnow_audio_sync.h

Comment on lines +1541 to +1546
if (audioSyncTransport == 1) {
const uint8_t syncChannel = WiFi.channel();
const bool amSender = (audioSyncEnabled & 0x01) != 0;
if (!espnowAudioSync::begin(syncChannel, amSender)) {
DEBUG_PRINTLN(F("[ENaudio] espnowAudioSync::begin() failed - falling back to UDP."));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The logged UDP fallback never actually happens.

If espnowAudioSync::begin() fails here, audioSyncTransport stays 1, so later send/receive code keeps taking the ESP-NOW branch and sync stops instead of falling back. Flip a runtime-ready flag back to UDP here, or gate the later transport branches on successful ESP-NOW init.

Minimal localized fix
       if (audioSyncTransport == 1) {
         const uint8_t syncChannel = WiFi.channel();
         const bool    amSender    = (audioSyncEnabled & 0x01) != 0;
         if (!espnowAudioSync::begin(syncChannel, amSender)) {
-          DEBUG_PRINTLN(F("[ENaudio] espnowAudioSync::begin() failed - falling back to UDP."));
+          audioSyncTransport = 0; // runtime fallback; persisted config stays unchanged until next save/reboot
+          DEBUG_PRINTLN(F("[ENaudio] espnowAudioSync::begin() failed - using UDP for this session."));
         }
       }
📝 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
if (audioSyncTransport == 1) {
const uint8_t syncChannel = WiFi.channel();
const bool amSender = (audioSyncEnabled & 0x01) != 0;
if (!espnowAudioSync::begin(syncChannel, amSender)) {
DEBUG_PRINTLN(F("[ENaudio] espnowAudioSync::begin() failed - falling back to UDP."));
}
if (audioSyncTransport == 1) {
const uint8_t syncChannel = WiFi.channel();
const bool amSender = (audioSyncEnabled & 0x01) != 0;
if (!espnowAudioSync::begin(syncChannel, amSender)) {
audioSyncTransport = 0; // runtime fallback; persisted config stays unchanged until next save/reboot
DEBUG_PRINTLN(F("[ENaudio] espnowAudioSync::begin() failed - using UDP for this session."));
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/audioreactive/audio_reactive.cpp` around lines 1541 - 1546, The
ESP-NOW init failure logs a UDP fallback but never actually flips the transport,
so later send/receive still use ESP-NOW; in the espnowAudioSync::begin() failure
branch either set the transport back to UDP (e.g. set audioSyncTransport = 0) or
set a dedicated runtime-ready flag (e.g. espnowReady = false) and update the
later transport decision points to check that flag (or call an
espnowAudioSync::isReady()) before taking the ESP-NOW branch; modify the code
paths that use audioSyncTransport to gate on the successful init flag (or the
updated audioSyncTransport) so the module truly falls back to UDP when begin()
fails (refer to audioSyncTransport, espnowAudioSync::begin, and the later
send/receive transport branches).

Comment on lines +97 to +98
/** @brief True once begin() has succeeded. Subsequent begin() calls no-op. */
static bool _initialised = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

begin() cannot refresh channel/peer state after a reconnect.

connected() is a reconnect hook, but after the first success _initialised makes every later begin() a no-op. In STANDALONE mode that leaves the pinned channel and broadcast peer registration frozen to the original values, so a later Wi-Fi reconnect on a different channel can strand ESP-NOW on stale state. Consider allowing begin() to reapply channel/peer settings when they change, or add an explicit reset/reinit path.

Also applies to: 191-195, 215-221

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/audioreactive/espnow_audio_sync.h` around lines 97 - 98, The static
flag _initialised makes begin() a no-op after the first success so connected()
can't refresh channel/peer state on reconnect; change begin() (and any
STANDALONE-mode init paths that check _initialised) to detect channel or peer
changes and reapply ESP-NOW configuration instead of returning early, or add a
clear/reset helper that forces reinitialisation and re-registers the broadcast
peer; specifically update the logic around _initialised, begin(), and the
reconnect hook connected() to either (1) compare current Wi‑Fi channel/peer info
and perform reconfiguration when different, or (2) expose and call a
reset/reinit function from connected() to unregister and re-register peers and
set the channel before reinitialising ESP-NOW.

@DedeHai
Copy link
Copy Markdown
Collaborator

DedeHai commented May 21, 2026

Thanks for contributing

I did not yet look at the code but just from my experience with ESPNow I know it is a bit of a "diva" and unless you know what you are doing and testing all possible scenarios it can run into a lot of issues. So my question is: how much of the code is AI and did you review and try tounderstand every line of code?
This question is just to know where this is at in terms of "ripeness"

@Gallosauro
Copy link
Copy Markdown
Author

Hello, it is mostly AI-generated. I tested on the hardware, and it is working fine, though.

@DedeHai
Copy link
Copy Markdown
Collaborator

DedeHai commented May 21, 2026

I just now read the first few lines of changes and I have to say that this wont fly at all in its current state, all I see is AI slop.

@DedeHai
Copy link
Copy Markdown
Collaborator

DedeHai commented May 21, 2026

Are you experienced enough in C++ to clean this up?

@softhack007 softhack007 added the AI Partly generated by an AI. Make sure that the contributor fully understands the code! label May 21, 2026
float* raw_buffer = (float*)heap_caps_malloc((2 * samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT);
if ((raw_buffer == nullptr)) return; // something went wrong
valFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
valFFT = (float*)heap_caps_aligned_calloc(16, 2 * samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why did you change this part? It has nothing to to with sound sync,

Copy link
Copy Markdown
Member

@softhack007 softhack007 left a comment

Choose a reason for hiding this comment

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

Hi @Gallosauro thanks for your contribution.

Before we can continue with code reviews, please align your code with the latest source code from main.

Currently your PR contains lots of deletions that revert some of our latest improvements. We cannot integrate your PR like this, and I don't want to spam with "please revert this change" comments 😉

@softhack007
Copy link
Copy Markdown
Member

Hello, it is mostly AI-generated. I tested on the hardware, and it is working fine, though.

oh-oh, this sounds like lots of work for reviewers 😉

@softhack007
Copy link
Copy Markdown
Member

Recommended setup:
Master on send, Slave on receive, ESPNOW, and in case of different pinout : CheckboxSettingBrightness✅Color✅Effects✅Palette / Effect arguments✅Main segment only✅Segment options / Segment bounds / Segment data❌ untickedApply incoming sync✅

Aehm ... this has nothing to do with Sound sync. Are you sure? And what does the "different pinout" remark want to say?

@softhack007
Copy link
Copy Markdown
Member

softhack007 commented May 21, 2026

This is the full explanation of the code from the AI:

Audio Reactive ESP-NOW Patch — Complete Edit Recipe Canonical single-doc version. Applies to a clean copy of WLED v16.0.0's usermods/audioreactive/audio_reactive.cpp (2307 lines). Eleven edits total — overlapping ones from the original two-doc workflow are now merged.

Please clean up the PR description. Parts like above text are just confusing, at least for me.

@softhack007 softhack007 added needs_rebase This PR needs to be re-based to the current development branch needs_rework PR needs improvements before merging (RED FLAG) slop low quality; something that deserves more care, love and attention and removed external Not part of WLED itself - an external plugin/remote etc. labels May 21, 2026
#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG))
#include <esp_timer.h>
#endif
#define ESPNOW_AUDIO_SYNC_SKIP_PACKET_DECL // audio_reactive.cpp already declares audioSyncPacket
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please don't re-define the audiosync packet structure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

edit: looks like this define is not referenced, please remove it.


infoArr = user.createNestedArray(F("ESP-NOW Mode"));
infoArr.add(espnowAudioSync::isHosted() ? F("hosted (sharing WLED stack)") : F("standalone"));
}
Copy link
Copy Markdown
Member

@softhack007 softhack007 May 21, 2026

Choose a reason for hiding this comment

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

please reduce the statistics lines added here - add only 2-3 lines that are absolutely needed. If you want more infos, put them behind #ifdef WLED_DEBUG ... #endif

// NEW: Audio Sync Transport selector (UDP vs ESP-NOW)
uiScript.print(F("dd=addDropdown(ux,'sync:transport');"));
uiScript.print(F("addOption(dd,'UDP (default)',0);"));
uiScript.print(F("addOption(dd,'ESP-NOW (low-latency)',1);"));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why do you think ESP-NOW is low-latency?

#include <esp_wifi.h>
#include <esp_timer.h>
#include <esp_idf_version.h>
#include <string.h>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

too many includes, please clean up

struct __attribute__((packed)) ENPacket {
uint16_t magic; ///< Equals EN_MAGIC (0xA53E).
uint8_t version; ///< Equals EN_VERSION (1).
uint8_t flags; ///< Bit 0: this is the duplicate copy.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

?

uint8_t version; ///< Equals EN_VERSION (1).
uint8_t flags; ///< Bit 0: this is the duplicate copy.
uint16_t seq; ///< Monotonically increasing per sender.
uint16_t pad; ///< Alignment pad (set to 0).
Copy link
Copy Markdown
Member

@softhack007 softhack007 May 21, 2026

Choose a reason for hiding this comment

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

padding in a packed struct? why?

* time (HOSTED mode); false when this module owns the radio
* (STANDALONE mode).
*/
static bool _hosted = false;
Copy link
Copy Markdown
Member

@softhack007 softhack007 May 21, 2026

Choose a reason for hiding this comment

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

can you explain why these two modes (hosted/self-hosted) are needed?

if (outLen < EN_PAYLOAD) return false;

uint32_t arrLocalUs;
portENTER_CRITICAL(&_rxMux);
Copy link
Copy Markdown
Member

@softhack007 softhack007 May 21, 2026

Choose a reason for hiding this comment

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

(major problem)
portENTER_CRITICAL() stalls everything, including LEDs drivers. If you need a mutex, please use a regular FreeRTOS semaphore instead.

if (diff > 1) { _stat_gaps += (diff - 1); }
}

portENTER_CRITICAL(&_rxMux);
Copy link
Copy Markdown
Member

@softhack007 softhack007 May 21, 2026

Choose a reason for hiding this comment

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

same as above - no portENTER_CRITICAL() please, use a semaphore if needed.

*/
static void _onRecv(ESPNOW_RECV_CB_SIG) {
ESPNOW_RECV_CB_ARGS_UNUSED
handleIncomingPacket(data, len);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What if an incoming packet is not for audio sync, but for esp-now remote or for WLED sync? Will these be droped, or not?

@softhack007
Copy link
Copy Markdown
Member

Generic comment: please make sure that esp-now sound sync is only added when WLED_DISABLE_ESPNOW is not defined. This also implies the "self-hosted" part of your PR can be removed.

If users build with -D WLED_DISABLE_ESPNOW the usermod should respect that.

Comment thread usermods/audioreactive/audio_reactive.cpp
Copy link
Copy Markdown
Member

@softhack007 softhack007 May 21, 2026

Choose a reason for hiding this comment

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

@coderabbitai please scan espnow_audio_sync.h‎ for unused variables, unused struct members, unneeded getter/setter functions (=only called from the main object), and dead or unreachable code.

This comment was marked as off-topic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== fftResultMax usage ==="
rg -n "fftResultMax" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== enDeadline usage ==="
rg -n "enDeadline" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== agcSensitivity usage ==="
rg -n "agcSensitivity" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== userVar1 usage ==="
rg -n "userVar1" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== useMainSegmentOnly usage in audioreactive ==="
rg -n "useMainSegmentOnly" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== `#if` 0 block bounds ==="
rg -n "`#if` 0|`#endif`" usermods/audioreactive/audio_reactive.cpp | head -30

echo ""
echo "=== udpSamplePeak usage ==="
rg -n "udpSamplePeak" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== receivedFormat usage ==="
rg -n "receivedFormat" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== last_UDPTime usage ==="
rg -n "last_UDPTime" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== binNum maxVol deprecated check ==="
rg -n "binNum|maxVol" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== getUMData callers - looking for only-called-from-main pattern ==="
rg -n "getUMData\|removeAudioPalettes\|createAudioPalettes\|fillAudioPalettes\|getCRGBForBand" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== syncVolumeSmth scope ==="
rg -n "syncVolumeSmth" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== addPalettes usage ==="
rg -n "addPalettes" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== palettes (int8_t) usage ==="
rg -n "\bpalettes\b" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== updateIsRunning usage ==="
rg -n "updateIsRunning" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== raw_buffer (old variable, should be gone) ==="
rg -n "raw_buffer" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== haveDoneFFT usage ==="
rg -n "haveDoneFFT" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== audioSyncTransport in ESP-NOW receive condition (loop) ==="
rg -n "audioSyncTransport\|udpSyncConnected" usermods/audioreactive/audio_reactive.cpp | head -40

echo ""
echo "=== Check for audioSyncEnabled receive mode gating with ESP-NOW ==="
rg -n "audioSyncEnabled" usermods/audioreactive/audio_reactive.cpp | head -40

Length of output: 8485


🏁 Script executed:

#!/bin/bash
echo "=== Brace count verification around connected() ==="
# Extract lines from connected() through loop() to count braces
python3 - <<'EOF'
with open("usermods/audioreactive/audio_reactive.cpp", "r") as f:
    lines = f.readlines()

# Find connected() start
start = None
for i, line in enumerate(lines):
    if "void connected() override" in line:
        start = i
        break

if start is None:
    print("connected() not found")
    exit()

print(f"connected() starts at line {start+1}")

depth = 0
in_comment = False
in_string = False
found_open = False

for i in range(start, min(start+200, len(lines))):
    line = lines[i].rstrip()
    for j, ch in enumerate(line):
        if in_comment:
            if ch == '*' and j+1 < len(line) and line[j+1] == '/':
                in_comment = False
            continue
        if not in_comment and ch == '/' and j+1 < len(line) and line[j+1] == '*':
            in_comment = True
            continue
        if ch == '/' and j+1 < len(line) and line[j+1] == '/':
            break  # rest of line is comment
        if ch == '{':
            depth += 1
            found_open = True
        elif ch == '}':
            depth -= 1
    
    if found_open and depth == 0:
        print(f"connected() closes at line {i+1}: {line}")
        print(f"Next line ({i+2}): {lines[i+1].rstrip() if i+1 < len(lines) else 'EOF'}")
        break
    
    if i > start + 5 and found_open and "void loop() override" in line:
        print(f"WARNING: found 'void loop() override' at line {i+1} with depth={depth} (not closed yet!)")
        print(f"  depth={depth}, context:")
        for k in range(max(0,i-3), min(len(lines), i+3)):
            print(f"  {k+1}: {lines[k].rstrip()}")
        break

EOF

echo ""
echo "=== Check `#if` 0 block location ==="
python3 - <<'EOF'
with open("usermods/audioreactive/audio_reactive.cpp", "r") as f:
    lines = f.readlines()

for i, line in enumerate(lines):
    if "`#if` 0" in line:
        print(f"Line {i+1}: {line.rstrip()}")
        # Show surrounding context
        for k in range(i, min(len(lines), i+5)):
            print(f"  {k+1}: {lines[k].rstrip()}")
        print()
EOF

echo ""
echo "=== fftResultMax - confirm no assignments ==="
rg -n "fftResultMax\s*\[" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== Check getJsonValue 3-arg usage ==="
rg -n "getJsonValue.*uint8_t.*0" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== Check for SR_DMTYPE_NETWORK_ONLY in loop() receive gate ==="
rg -n -A2 -B2 "audioSyncEnabled.*0x02.*udpSyncConnected" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== connectUDPSoundSync - does it open UDP even for ESPNOW? ==="
rg -n -A5 "connectUDPSoundSync" usermods/audioreactive/audio_reactive.cpp | head -40

Length of output: 2866


@softhack007, here are the findings from scanning the file. I'll start with the most critical item.


🔴 Critical: Missing closing } for connected() — compilation error

The diff removes the original closing brace of connected() (the - } line) when inserting the ESP-NOW begin() block, but never puts it back. The brace-depth counter confirms that void loop() override is reached at line 1558 while depth is still 1 (i.e., still inside connected()). This is a C++ syntax error and should prevent compilation.

The fix is simply inserting } to close connected() before the loop() comment:

      if (audioSyncTransport == 1) {
        const uint8_t syncChannel = WiFi.channel();
        const bool    amSender    = (audioSyncEnabled & 0x01) != 0;
        if (!espnowAudioSync::begin(syncChannel, amSender)) {
          DEBUG_PRINTLN(F("[ENaudio] espnowAudioSync::begin() failed - falling back to UDP."));
        }
      }
    }  // ← missing closing brace for connected()

    /*
     * loop() is called continuously...
     */
    void loop() override

🟠 Unused variable: enDeadline in receiveAudioData()

Lines 1314–1315: enDeadline is declared, populated by espnowAudioSync::poll(), and then silently discarded — the returned timing deadline is never consumed:

uint32_t enDeadline = 0;
if (espnowAudioSync::poll(&enFrame, sizeof(enFrame), &enDeadline, /*renderDelayUs=*/15000)) {
    decodeAudioData(...);
    // enDeadline is never used here

If the intent is to schedule rendering at a precise time (the whole point of the deadline), this feature is non-functional. Either use the value (e.g., for a delayMicroseconds-style render-timing nudge) or drop it and pass nullptr as the deadline argument.


🟡 Dead variable: fftResultMax[] (line 232, SR_DEBUG only)

Declared but never written to or read from anywhere in the file:

`#ifdef` SR_DEBUG
static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing...
`#endif`

No assignment, no reference. Safe to remove.


🟡 Dead code: #if 0 block — old FFT bin mapping (line 491)

"Andrew's updated mapping" for a 10240 Hz sample rate is permanently disabled via #if 0. It's been superseded by the softhack007 22050 Hz mapping in the #else branch and carries no informational value that the comment doesn't already convey. Worth removing to reduce maintenance surface.

There is a second, smaller #if 0 block at line 929 (a debug print loop inside FFT_SAMPLING_LOG) that is also dead.


🟡 Commented-out code that can be cleaned up

Location Item
Line 104 //static float agcSensitivity = 128; — the getSensitivity() function it references was never implemented; safe to delete
Lines 1272, 1300 //userVar1 = samplePeak; in both decodeAudioData() variants — userVar1 doesn't exist in the current WLED API; safe to delete

🔵 Design note: UDP socket opened even in ESP-NOW transport mode

connectUDPSoundSync() and connected() unconditionally open a UDP multicast socket whenever audioSyncEnabled != 0, regardless of audioSyncTransport. In practice this means:

  • udpSyncConnected is true even when ESP-NOW is the selected transport — which is why the loop() receive gate if ((audioSyncEnabled & 0x02) && udpSyncConnected) correctly reaches receiveAudioData() in ESP-NOW mode (the function then early-returns with the ESP-NOW path). Not broken, but wastes a multicast socket and generates spurious fftUdp.flush() calls. Worth skipping UDP socket setup when audioSyncTransport == 1.

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

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

Learnt from: softhack007
Repo: wled/WLED PR: 5559
File: wled00/wled.h:990-998
Timestamp: 2026-05-03T11:37:43.177Z
Learning: In the WLED audioreactive usermod (usermods/audioreactive/*), when using DEBUGSR_PRINT, DEBUGSR_PRINTLN, or DEBUGSR_PRINTF, use the function-style macros DEBUGOUT(x), DEBUGOUTLN(x), and DEBUGOUTF(x...) instead of the old stream-style form (DEBUGOUT.print(...)/println(...)/printf(...)). This ensures consistent logging behavior and avoids deprecated syntax.

Learnt from: softhack007
Repo: wled/WLED PR: 4843
File: usermods/elastic_collisions/Elastic_Collisions.cpp:1-1
Timestamp: 2026-05-06T22:10:27.517Z
Learning: In C/C++, do not flag precedence issues for expressions that mix additive operators (`+`, `-`) with shift operators (`<<`, `>>`) based on the assumption that shifts bind tighter. Per C/C++ operator precedence, `+`/`-` have higher precedence than `<<`/`>>` (e.g., `x - edge0 << 8` parses as `(x - edge0) << 8`). When reviewing WLED (and other) fixed-point/bit-manipulation code, confirm the intended parse using cppreference before reporting a precedence bug for mixed `-`/`+` and `<<`/`>>` expressions.

Learnt from: softhack007
Repo: wled/WLED PR: 5599
File: usermods/audioreactive/audio_reactive.cpp:1303-1308
Timestamp: 2026-05-11T16:00:13.574Z
Learning: When targeting arduino-esp32 v3.x (ESP-IDF 5.x), treat `WiFiUDP::flush()` as deprecated and ineffective for RX draining; it should not be used to clear/drain the UDP receive buffer. Instead, use `WiFiUDP::clear()`, which was introduced in arduino-esp32 v3.0 and is marked as the deprecated replacement via `NetworkUdp.h`.

If the code must compile and behave correctly across both arduino-esp32 v2.x (ESP-IDF < 5) and v3.x (ESP-IDF >= 5), use conditional compilation:
- for `ESP_IDF_VERSION_MAJOR < 5`: use `udp.flush()`
- for `ESP_IDF_VERSION_MAJOR >= 5`: use `udp.clear()`

Do not flag `WiFiUDP::clear()` usages for ESP-IDF >= 5 as non-existent—this is the documented correct replacement.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@coderabbitai please scan espnow_audio_sync.h‎ for unused variables, unused struct members, unneeded getter/setter functions (=only called from the main object), and dead or unreachable code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

Good question — let me search the codebase to cross-reference every symbol from the header against its actual usage sites.


🏁 Script executed:

#!/bin/bash
echo "=== espnowAudioSync API calls in audio_reactive.cpp ==="
rg -n "espnowAudioSync::" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== espnowAudioSync API calls across the whole codebase ==="
rg -rn "espnowAudioSync::"

echo ""
echo "=== _rxSendUs usage ==="
rg -n "_rxSendUs" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== ENPacket::pad usage ==="
rg -n "\.pad" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== Stats struct field usage (dupes, gaps referenced from stats()) ==="
rg -n "stats\(\)" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== isHosted() usage ==="
rg -rn "isHosted"

echo ""
echo "=== _stat_dupes usage ==="
rg -n "_stat_dupes" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== renderDelayUs usage in poll() ==="
rg -n "renderDelayUs\|deadline_us\|poll(" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== EN_PREAMBLE constant usage ==="
rg -n "EN_PREAMBLE" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== BROADCAST_MAC const usage ==="
rg -n "BROADCAST_MAC" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== _channel usage ==="
rg -n "_channel" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== _hosted usage ==="
rg -n "_hosted" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== _rxSendUs: set vs read ==="
grep -n "_rxSendUs" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== deadline_us parameter usage in poll() and its callers ==="
rg -rn "deadline_us\|poll("

echo ""
echo "=== espnowChannel usage ==="
rg -rn "espnowChannel\|espnow_channel\|audioSyncTransport\|_syncTransport"

Length of output: 3579


Let me do one more targeted check before the final report.


🏁 Script executed:

#!/bin/bash
echo "=== How stats() fields are consumed in audio_reactive.cpp ==="
rg -n "s\.rx\|s\.dupes\|s\.gaps\|s\.tx\|stat_" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== Context around stats() call (±15 lines) ==="
rg -n -A 20 "auto s = espnowAudioSync" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== Check ep.pad and memset ordering in espnow_audio_sync.h ==="
grep -n "memset\|ep\.pad\|ep\.flags\|ep\.seq\|ep\.magic" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== Check _rxSendUs read sites (grep wider) ==="
grep -n "rxSendUs\|send_us" usermods/audioreactive/espnow_audio_sync.h
grep -rn "rxSendUs\|send_us" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== Check renderDelayUs / 2 logic and how enDeadline is used ==="
rg -n "enDeadline\|deadline_us\|renderDelay" usermods/audioreactive/audio_reactive.cpp

echo ""
echo "=== Any direct read of ENPacket::pad on receiver side ==="
rg -n "ep->pad\|ep\.pad" usermods/audioreactive/espnow_audio_sync.h

echo ""
echo "=== Stats::dupes field reference outside of stats() ==="
rg -rn "\.dupes"

Length of output: 1823


@softhack007 Here is the full scan result. I found three confirmed dead-code items and one item to double-check in the caller.


1. _rxSendUs — Write-only dead variable ✂️

Lines 117 and 357.

static uint32_t  _rxSendUs = 0;          // line 117 — declared
// ...
_rxSendUs = ep->send_us;                  // line 357 — written in handleIncomingPacket()

_rxSendUs is never read. poll() computes the deadline exclusively from _rxArrivedLocalUs; the sender timestamp that was carefully captured from ep->send_us is silently discarded. Either:

  • Remove _rxSendUs and the assignment on line 357 (simplest), or
  • Actually use it in poll() to compute a network-latency-compensated deadline (arrLocalUs - (arrLocalUs - rxSendUs) gives round-trip offset) — which was presumably the original intent.

2. Stats::dupes field — Populated but never consumed ✂️

Lines 135, 348, 395.

static uint32_t _stat_dupes = 0;              // declared
// ...
if (diff <= 0) { _stat_dupes++; return true; } // incremented
// ...
return Stats{_stat_rx, _stat_dupes, _stat_gaps, _stat_tx, _stat_tx_err}; // returned

In audio_reactive.cpp, the stats() result is used as follows — s.dupes is the only field never referenced:

auto s = espnowAudioSync::stats();
infoArr.add(s.rx);         // ✔ used
// s.gaps used in frame-loss formula  ✔
infoArr.add(s.tx);         // ✔ used
// s.tx_err checked (> 0)  ✔
// s.dupes — never touched ✗

Either add it to the info pane (e.g. as "ESP-NOW Dupes Dropped") or remove _stat_dupes, the increment in handleIncomingPacket(), and Stats::dupes.


3. ep.pad = 0 in send() — Redundant assignment ✂️

Lines 255 and 260.

memset(&ep, 0, sizeof(ep));   // line 255 — zeroes the entire struct
ep.magic   = EN_MAGIC;
ep.version = EN_VERSION;
ep.flags   = 0;               // line 258 — redundant (already 0)
ep.seq     = ++_txSeq;
ep.pad     = 0;               // line 260 — redundant (already 0)

memset at line 255 already zeroes every byte of ep, so ep.pad = 0 (and ep.flags = 0) are no-ops. ep.flags = 0 is at least visually useful before being set to 1 for the duplicate copy; ep.pad = 0 contributes nothing at all. Remove line 260.


4. enDeadline in audio_reactive.cpp — Double-check the caller ⚠️

The poll() call on line 1315 passes &enDeadline:

if (espnowAudioSync::poll(&enFrame, sizeof(enFrame), &enDeadline, /*renderDelayUs=*/15000)) {

However, the search for any subsequent use of enDeadline returned no results (the rg regex had a syntax error, so this is not conclusive). It is worth verifying that enDeadline is actually acted on after poll() returns — if it is declared but then never read, the entire deadline_us / renderDelayUs machinery in poll() is dead from the caller's perspective as well.


Non-issues (for completeness)

Symbol Status
EN_PREAMBLE Only used to define EN_PACKET; a named intermediate constant — not harmful, fine to keep for readability.
ENPacket::pad struct field Structurally needed for the static_assert layout; the explicit assignment is redundant (see item 3), but the field itself should stay.
All other file-scope statics (_channel, _hosted, _rxReady, _rxArrivedLocalUs, _lastRxSeq, _hasLastRxSeq, _rxMux, _stat_rx, _stat_gaps, _stat_tx, _stat_tx_err) All legitimately used.
begin(), send(), poll(), handleIncomingPacket(), isHosted(), stats() All called from audio_reactive.cpp.

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

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

Learnt from: softhack007
Repo: wled/WLED PR: 4843
File: usermods/elastic_collisions/Elastic_Collisions.cpp:1-1
Timestamp: 2026-05-06T22:10:27.517Z
Learning: In C/C++, do not flag precedence issues for expressions that mix additive operators (`+`, `-`) with shift operators (`<<`, `>>`) based on the assumption that shifts bind tighter. Per C/C++ operator precedence, `+`/`-` have higher precedence than `<<`/`>>` (e.g., `x - edge0 << 8` parses as `(x - edge0) << 8`). When reviewing WLED (and other) fixed-point/bit-manipulation code, confirm the intended parse using cppreference before reporting a precedence bug for mixed `-`/`+` and `<<`/`>>` expressions.

missing `}`

don't people try to compile before making a PR?
@coderabbitai coderabbitai Bot added the external Not part of WLED itself - an external plugin/remote etc. label May 21, 2026
@softhack007 softhack007 removed the external Not part of WLED itself - an external plugin/remote etc. label May 21, 2026
* before.
*
* Features:
* - ESP-NOW broadcast (~2-5 ms one-way vs ~8-30 ms UDP/Wi-Fi)
Copy link
Copy Markdown
Member

@softhack007 softhack007 May 21, 2026

Choose a reason for hiding this comment

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

Just for explaination: the observed delays are only a minor problem. sound sync sends one packet each 22millis, and the listed network delays (wifi included) are both the same magnitude, so not going to add noticeable lagging.

"Wifi sleep" - if enabled - is actually the relevant "realtime killer". If just one client on your wifi allows sleep mode, the AP/router is allowed to send udp data in batches, with up to 500ms between batches.

ep.flags = 0;
ep.seq = ++_txSeq;
ep.pad = 0;
ep.send_us = (uint32_t)(esp_timer_get_time() & 0xFFFFFFFFULL);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a microsecond timer, it will roll-over about once per hour. Are you sure?

_stat_rx++;
memcpy(_rxPayload, ep->payload, EN_PAYLOAD);
_rxSendUs = ep->send_us;
_rxArrivedLocalUs = (uint32_t)(esp_timer_get_time() & 0xFFFFFFFFULL);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What do you want to measure here? Getting two esp32 into time sync on microseconds level is an impossible mission....

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

not impossible but futile ;)

@DedeHai
Copy link
Copy Markdown
Collaborator

DedeHai commented May 22, 2026

@softhack007 I did not really look into this code: is the general approach viable? From glancing over the code it looks like its a full re-implementation of ESPnow code and duplicates what is already implemented in the main code - I do know there is a hook for receiving messages, I do not know if there is one for sending them but that would be the proper way instead of making AR fully incompatible with any other ESPNow functionality.

@softhack007
Copy link
Copy Markdown
Member

softhack007 commented May 22, 2026

@DedeHai yes I agree, I also have doubts about the general approach.

  • Adding a new "transport" settings option is a good idea - there were also suggestions in the past for using "UDP boadcast" instead of "UDP multicast" as the transport protocol, so esp-now would fit into this picture.
  • 8266 not supported - receiving should work on 8266 with the existing callback hooks. Only esp32 can be a sender.
  • Receiving should only rely on the onEspNowMessage() callback hook. The hooked function can check the message format and return false in case of "not meant for AR", otherwise directly unpack with decodeAudioData() - no need to pass this on to the main loop.
  • I'm not sure if "sending" actually needs a dedicated implementation - it would be better if AR calls the same functions that ESP-NOW based WLED Sync already provides.
  • De-duplication might be a specific topic for esp-now, however the timestamp / sequence ID approach in this PR might cause more troubles than it brings benefit. WLED-MM AR already has an optional sequence-check implemented, so adding another one on esp-now level might become a technical debt for the future.
  • UDP sound sync must be inactive while running on ESP-NOW - this is not addressed in the PR.
  • A lot more testing is needed - including corner cases and different WiFi / ethernet / AP-less setups, multiple senders, etc.

My Conclusion

  • Some good new ideas
  • Avoid a full re-implementation of ESPnow within audioreactive. Better re-use existing esp-now sending code and receiving callbacks.
  • 8266 support is needed for esp-now receiving.
  • This PR only works for one user setup, and I already see lots of troubles and forgotten cases. The "critical section" that may cause LEDs flickering on -C3 is just one example.
  • I'm not sure if @Gallosauro has good enough experience with C++ (and in working with github) to continue adjusting the AI code in this PR. We cannot rely on an AI agent alone - the topic is much more complicated than "works for me" and "let's vibe-code it".
  • (minor) we cannot merge this PR as it stands - @Gallosauro started with "add files via upload", and this unfortunately reverts changes we already made after 16.0.0 was released.

@softhack007
Copy link
Copy Markdown
Member

For completeness - I've found two PR that also suggest a way for adding esp-now in AR. They all need rework but can be examples for learning what's needed.

@Gallosauro
Copy link
Copy Markdown
Author

Thanks, everyone, for the comments. I will work from that. No, I do not have enough experience in C++, but I will try to see from these comments if I can do anything. In my case solved the issue of connecting multiple cards, which failed with the UDP connection. This code implementation also works with the espnow normal function, and if a command is sent will receive it on the slave card. In the menu, by selecting ESPnow instead of UDP, it automatically deselects it to answer the question. Regarding the C3, no testing has been done yet.
Again, thanks everyone for the amazing job with Wled.

@DedeHai DedeHai marked this pull request as draft May 22, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Partly generated by an AI. Make sure that the contributor fully understands the code! needs_rebase This PR needs to be re-based to the current development branch needs_rework PR needs improvements before merging (RED FLAG) slop low quality; something that deserves more care, love and attention usermod usermod related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants