Skip to content

Replace QuickESPNow with local library#5624

Open
DedeHai wants to merge 4 commits into
wled:mainfrom
DedeHai:ESPNow_nolib
Open

Replace QuickESPNow with local library#5624
DedeHai wants to merge 4 commits into
wled:mainfrom
DedeHai:ESPNow_nolib

Conversation

@DedeHai
Copy link
Copy Markdown
Collaborator

@DedeHai DedeHai commented May 16, 2026

QuickESPNow is not well suited as it is focused on compatibility rather than speed and resources.

This PR replaces it with local functions directly based on esp API calls.
From my tests it is faster and more light weight:

  • ESP32 shows a saving of ~10k of heap
  • ESP8266 shows a saving of ~1.5k of heap

I tested this with a send and receive function both with large packets and bursts of small packets. While large packets work quite well, small (5 bytes) packets tend to get lost on ESP8266 when sent in bursts longer than 6 packets.
The changes need testing in real applications but bench-top tests are very promising.

Summary by CodeRabbit

Release Notes

  • Refactor
    • Replaced external ESP-NOW library with internal implementation for improved compatibility across ESP8266 and ESP32 platforms
    • Streamlined ESP-NOW wireless communication handling and device-to-device connectivity

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Walkthrough

This PR replaces the external QuickESPNow library dependency with a custom WledEspNow abstraction layer. The new implementation consolidates platform-specific ESP-NOW code, provides unified callback-based APIs for send/receive, and updates all initialization and usage points across the WLED codebase.

Changes

ESP-NOW Abstraction Migration

Layer / File(s) Summary
New ESP-NOW abstraction and implementation
wled00/src/dependencies/espnow_wled/espnow_wled.h, wled00/src/dependencies/espnow_wled/espnow_wled.cpp
Introduces WledEspNow class with callback registration (onDataSent, onDataRcvd), lifecycle methods (begin() with channel/iface and STA-mode overload, stop()), and broadcast-only send(). Implements platform-specific sent/receive callbacks with conditional handling for ESP32 IDF v5+ esp_now_recv_info_t path and ESP8266 RSSI extraction. Includes commented-out WledEspNowBroadcast wrapper design (unreviewed, untested).
Unified include consolidation in wled.h
wled00/wled.h
Removes platform-specific ESPNOW header includes and QuickEspNow headers for both ESP8266 and ESP32, and replaces with single unified #include of src/dependencies/espnow_wled/espnow_wled.h guarded by WLED_DISABLE_ESPNOW.
Initialization and packet sending integration
wled00/wled.cpp, wled00/udp.cpp
Updates WLED::initConnection() to register callbacks and call espNow.begin() for both AP and STA modes; switches shutdown to call espNow.stop(). Updates notify() in udp.cpp to use espNow.send() instead of quickEspNow.send() for multi-segment ESP-NOW packet transmission.
Remove QuickESPNow from build dependencies
platformio.ini
Deletes blazoncek/QuickESPNow.git#optional-debug entries from both ESP8266 dependency lists.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

connectivity

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Replace QuickESPNow with local library' directly and clearly summarizes the main change: removing the QuickESPNow external dependency and replacing it with a local ESP-NOW implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

❤️ Share

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

@coderabbitai coderabbitai Bot added the connectivity Issue regarding protocols, WiFi connection or availability of interfaces label May 16, 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: 3

🧹 Nitpick comments (1)
wled00/src/dependencies/espnow_wled/espnow_wled.cpp (1)

230-230: ⚡ Quick win

Use debug macros instead of unconditional Serial.printf.

Line 230 bypasses WLED_DEBUG gating and adds serial I/O in normal builds.

Suggested fix
-  if (err != 0 && isretransmit) Serial.printf("ESP-NOW send failed with error %d, inflight=%d\n", err, (int)espNow._inFlight);
+  if (err != 0 && isretransmit) DEBUG_PRINTF_P(PSTR("ESP-NOW send failed with error %d, inflight=%d\n"), err, (int)espNow._inFlight);
As per coding guidelines: "Use `DEBUG_PRINTF()` / `DEBUG_PRINTLN()` for debug output (compiled out unless `-D WLED_DEBUG`)."
🤖 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 `@wled00/src/dependencies/espnow_wled/espnow_wled.cpp` at line 230, Replace the
unconditional Serial.printf in espnow_wled.cpp (the ESP-NOW send failure log in
the send callback/handler around the if (err != 0 && isretransmit) check) with
the project debug macro so it is compiled out unless WLED_DEBUG is defined; use
DEBUG_PRINTF (or DEBUG_PRINTLN if preferred) to emit the same formatted message
("ESP-NOW send failed with error %d, inflight=%d\n") and preserve the
(int)espNow._inFlight cast and err variable so the debug output remains
identical but gated by WLED_DEBUG.
🤖 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 `@wled00/src/dependencies/espnow_wled/espnow_wled.cpp`:
- Around line 221-228: The retry branch that calls esp_now_send(...) when
isretransmit is set does not increment the in-flight counter, causing _inFlight
to go out of sync; update the block that contains the
esp_now_send(const_cast<uint8_t*>(BCAST), const_cast<uint8_t*>(data), len) call
so that after a successful send (check err == ESP_OK) you increment _inFlight,
e.g., modify the retry path around variables _inFlight and isretransmit to
++_inFlight when the send returns success to keep the in-flight counter
accurate.
- Around line 186-190: The ESP8266 branch currently ignores return values from
esp_now_register_recv_cb(_espnowRecvCB),
esp_now_register_send_cb(_espnowSentCB), and
esp_now_add_peer(const_cast<uint8_t*>(BCAST), ESP_NOW_ROLE_COMBO, channel,
nullptr, 0) and sets _running = true unconditionally; change this to check each
call's return code (like the ESP32 path), log or handle failures, avoid setting
_running when any registration/peer-add fails, and return/clean up appropriately
(e.g., deinit or unregister) on error so subsequent send/stop operations behave
consistently.

In `@wled00/src/dependencies/espnow_wled/espnow_wled.h`:
- Around line 7-11: The conditional include for ESP-NOW uses a generic `#else`
which can incorrectly include <esp_now.h> on non-target platforms; update the
guard to explicitly check for the ESP32 architecture by replacing the `#else` with
`#elif` defined(ARDUINO_ARCH_ESP32) so the block reads `#ifdef` ESP8266 / `#elif`
defined(ARDUINO_ARCH_ESP32) / `#endif` and include <espnow.h> for ESP8266 and
<esp_now.h> for ESP32 (references: macros ESP8266, ARDUINO_ARCH_ESP32 and the
include directives <espnow.h> and <esp_now.h>).

---

Nitpick comments:
In `@wled00/src/dependencies/espnow_wled/espnow_wled.cpp`:
- Line 230: Replace the unconditional Serial.printf in espnow_wled.cpp (the
ESP-NOW send failure log in the send callback/handler around the if (err != 0 &&
isretransmit) check) with the project debug macro so it is compiled out unless
WLED_DEBUG is defined; use DEBUG_PRINTF (or DEBUG_PRINTLN if preferred) to emit
the same formatted message ("ESP-NOW send failed with error %d, inflight=%d\n")
and preserve the (int)espNow._inFlight cast and err variable so the debug output
remains identical but gated by WLED_DEBUG.
🪄 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: 09fa15a1-2b61-4741-a209-3454a4ae2277

📥 Commits

Reviewing files that changed from the base of the PR and between d337c8a and 32901fb.

📒 Files selected for processing (6)
  • platformio.ini
  • wled00/src/dependencies/espnow_wled/espnow_wled.cpp
  • wled00/src/dependencies/espnow_wled/espnow_wled.h
  • wled00/udp.cpp
  • wled00/wled.cpp
  • wled00/wled.h
💤 Files with no reviewable changes (1)
  • platformio.ini

Comment on lines +186 to +190
esp_now_set_self_role(ESP_NOW_ROLE_COMBO); // TODO: found no official documentation on this... quickespnow ESP_NOW_ROLE_SLAVE in STA mode and ESP_NOW_ROLE_CONTROLLER in AP mode
esp_now_register_recv_cb(_espnowRecvCB);
esp_now_register_send_cb(_espnowSentCB);
esp_now_add_peer(const_cast<uint8_t*>(BCAST), ESP_NOW_ROLE_COMBO, channel, nullptr, 0);
_running = true;
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

🧩 Analysis chain

🏁 Script executed:

cat -n wled00/src/dependencies/espnow_wled/espnow_wled.cpp | sed -n '175,200p'

Repository: wled/WLED

Length of output: 1059


🏁 Script executed:

cat -n wled00/src/dependencies/espnow_wled/espnow_wled.cpp | sed -n '150,210p'

Repository: wled/WLED

Length of output: 2527


🏁 Script executed:

find . -name "espnow_wled.h" -o -name "espnow_wled.hpp" | head -5

Repository: wled/WLED

Length of output: 105


🏁 Script executed:

cat wled00/src/dependencies/espnow_wled/espnow_wled.h | grep -A 20 "init\|start\|begin"

Repository: wled/WLED

Length of output: 2168


🏁 Script executed:

rg "esp_now_register_recv_cb|esp_now_register_send_cb|esp_now_add_peer" -A 2 -B 2

Repository: wled/WLED

Length of output: 1961


Add error checks for ESP8266 callback and peer registration before marking as running.

The ESP8266 code path (lines 187–189) ignores return codes from esp_now_register_recv_cb(), esp_now_register_send_cb(), and esp_now_add_peer(), while the ESP32 path properly checks them. If any call fails, _running still becomes true and the function returns success, causing inconsistent behavior in subsequent send/stop operations.

Suggested fix
  esp_now_set_self_role(ESP_NOW_ROLE_COMBO); // TODO: found no official documentation on this... quickespnow ESP_NOW_ROLE_SLAVE in STA mode and ESP_NOW_ROLE_CONTROLLER in AP mode
- esp_now_register_recv_cb(_espnowRecvCB);
- esp_now_register_send_cb(_espnowSentCB);
- esp_now_add_peer(const_cast<uint8_t*>(BCAST), ESP_NOW_ROLE_COMBO, channel, nullptr, 0);
+ if (esp_now_register_recv_cb(_espnowRecvCB) != 0) {
+   esp_now_deinit();
+   return false;
+ }
+ if (esp_now_register_send_cb(_espnowSentCB) != 0) {
+   esp_now_unregister_recv_cb();
+   esp_now_deinit();
+   return false;
+ }
+ if (esp_now_add_peer(const_cast<uint8_t*>(BCAST), ESP_NOW_ROLE_COMBO, channel, nullptr, 0) != 0) {
+   esp_now_unregister_recv_cb();
+   esp_now_unregister_send_cb();
+   esp_now_deinit();
+   return false;
+ }
  _running = true;
  return true;
🤖 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 `@wled00/src/dependencies/espnow_wled/espnow_wled.cpp` around lines 186 - 190,
The ESP8266 branch currently ignores return values from
esp_now_register_recv_cb(_espnowRecvCB),
esp_now_register_send_cb(_espnowSentCB), and
esp_now_add_peer(const_cast<uint8_t*>(BCAST), ESP_NOW_ROLE_COMBO, channel,
nullptr, 0) and sets _running = true unconditionally; change this to check each
call's return code (like the ESP32 path), log or handle failures, avoid setting
_running when any registration/peer-add fails, and return/clean up appropriately
(e.g., deinit or unregister) on error so subsequent send/stop operations behave
consistently.

Comment on lines +221 to +228
else if (_inFlight > 0 && !isretransmit) {
uint8_t lastInFlight = _inFlight;
delay(2); // wait for a queued message to be sent, found that 2ms is usually enough, dont want to be too cautios (burst send is currently an edge case)
// note: delay and general approach might need some tweaking for real world use, based on burst tests sending 16 messages
if (_inFlight < lastInFlight) {
isretransmit = true; // try once more
err = esp_now_send(const_cast<uint8_t*>(BCAST), const_cast<uint8_t*>(data), len); // A message was sent and the sent callback was called, so we can retry now.
}
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

Increment _inFlight after a successful retry send.

On Line 227 retry success, _inFlight is never incremented, so the in-flight counter drifts low and throttling becomes inaccurate.

Suggested fix
     if (_inFlight < lastInFlight) {
       isretransmit = true; // try once more
       err = esp_now_send(const_cast<uint8_t*>(BCAST), const_cast<uint8_t*>(data), len);  // A message was sent and the sent callback was called, so we can retry now.
+      if (err == 0) _inFlight++;
     }
📝 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
else if (_inFlight > 0 && !isretransmit) {
uint8_t lastInFlight = _inFlight;
delay(2); // wait for a queued message to be sent, found that 2ms is usually enough, dont want to be too cautios (burst send is currently an edge case)
// note: delay and general approach might need some tweaking for real world use, based on burst tests sending 16 messages
if (_inFlight < lastInFlight) {
isretransmit = true; // try once more
err = esp_now_send(const_cast<uint8_t*>(BCAST), const_cast<uint8_t*>(data), len); // A message was sent and the sent callback was called, so we can retry now.
}
else if (_inFlight > 0 && !isretransmit) {
uint8_t lastInFlight = _inFlight;
delay(2); // wait for a queued message to be sent, found that 2ms is usually enough, dont want to be too cautios (burst send is currently an edge case)
// note: delay and general approach might need some tweaking for real world use, based on burst tests sending 16 messages
if (_inFlight < lastInFlight) {
isretransmit = true; // try once more
err = esp_now_send(const_cast<uint8_t*>(BCAST), const_cast<uint8_t*>(data), len); // A message was sent and the sent callback was called, so we can retry now.
if (err == 0) _inFlight++;
}
🤖 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 `@wled00/src/dependencies/espnow_wled/espnow_wled.cpp` around lines 221 - 228,
The retry branch that calls esp_now_send(...) when isretransmit is set does not
increment the in-flight counter, causing _inFlight to go out of sync; update the
block that contains the esp_now_send(const_cast<uint8_t*>(BCAST),
const_cast<uint8_t*>(data), len) call so that after a successful send (check err
== ESP_OK) you increment _inFlight, e.g., modify the retry path around variables
_inFlight and isretransmit to ++_inFlight when the send returns success to keep
the in-flight counter accurate.

Comment on lines +7 to +11
#ifdef ESP8266
#include <espnow.h>
#else
#include <esp_now.h>
#endif
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

🧩 Analysis chain

🏁 Script executed:

# Locate the file
fd -type f "espnow_wled.h" 2>/dev/null

Repository: wled/WLED

Length of output: 35


🏁 Script executed:

# Check repository structure for espnow-related files
find . -name "*espnow*" -type f 2>/dev/null | head -20

Repository: wled/WLED

Length of output: 159


🏁 Script executed:

# Look for platform-conditional include patterns in the codebase
rg "`#ifdef`.*ESP8266" -A 3 -B 1 | head -100

Repository: wled/WLED

Length of output: 4376


🏁 Script executed:

cat -n wled00/src/dependencies/espnow_wled/espnow_wled.h | head -30

Repository: wled/WLED

Length of output: 1286


🏁 Script executed:

# Check if esp_now.h is referenced elsewhere in the codebase
rg "esp_now\.h|espnow\.h" -i

Repository: wled/WLED

Length of output: 359


🏁 Script executed:

# Check how other platform-specific includes are guarded in the project
rg "`#ifdef` (ARDUINO_ARCH|ESP32|ESP8266)" -B 1 -A 3 | head -150

Repository: wled/WLED

Length of output: 7549


🏁 Script executed:

# Verify the guideline text is correctly cited
grep -r "Wrap platform-conditional includes" . 2>/dev/null

Repository: wled/WLED

Length of output: 35


🏁 Script executed:

# Check if ARDUINO_ARCH_ESP32 is used elsewhere in the codebase for `#ifdef` checks
rg "defined\(ARDUINO_ARCH_ESP32\)|`#ifdef` ARDUINO_ARCH_ESP32" | head -20

Repository: wled/WLED

Length of output: 2098


Guard ESP-NOW header includes by explicit architecture checks.

Lines 7-11 use a generic #else that allows any non-ESP8266 build to fall through to #include <esp_now.h>, which does not match the coding guideline requiring explicit architecture guards. Use #elif defined(ARDUINO_ARCH_ESP32) instead of #else to prevent silent fallthrough on non-target platforms.

Suggested fix
-#ifdef ESP8266
-  `#include` <espnow.h>
-#else
-  `#include` <esp_now.h>
-#endif
+#ifdef ESP8266
+  `#include` <espnow.h>
+#elif defined(ARDUINO_ARCH_ESP32)
+  `#include` <esp_now.h>
+#endif

Per coding guidelines: "Wrap platform-conditional includes in #ifdef ARDUINO_ARCH_ESP32 / #ifdef ESP8266."

🧰 Tools
🪛 Clang (14.0.6)

[error] 10-10: 'esp_now.h' file not found

(clang-diagnostic-error)

🤖 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 `@wled00/src/dependencies/espnow_wled/espnow_wled.h` around lines 7 - 11, The
conditional include for ESP-NOW uses a generic `#else` which can incorrectly
include <esp_now.h> on non-target platforms; update the guard to explicitly
check for the ESP32 architecture by replacing the `#else` with `#elif`
defined(ARDUINO_ARCH_ESP32) so the block reads `#ifdef` ESP8266 / `#elif`
defined(ARDUINO_ARCH_ESP32) / `#endif` and include <espnow.h> for ESP8266 and
<esp_now.h> for ESP32 (references: macros ESP8266, ARDUINO_ARCH_ESP32 and the
include directives <espnow.h> and <esp_now.h>).

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

Labels

connectivity Issue regarding protocols, WiFi connection or availability of interfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant