Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,6 @@ lib_deps =
marvinroger/AsyncMqttClient @ 0.9.0
# for I2C interface
;Wire
# ESP-NOW library
;gmag11/QuickESPNow @ ~0.7.0
https://github.com/blazoncek/QuickESPNow.git#optional-debug
#For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line
#TFT_eSPI
#For compatible OLED display uncomment following
Expand Down Expand Up @@ -255,7 +252,6 @@ lib_deps_compat =
ESP8266PWM
IRremoteESP8266 @ 2.8.2
makuna/NeoPixelBus @ 2.7.9
https://github.com/blazoncek/QuickESPNow.git#optional-debug
https://github.com/tignioj/ArduinoUZlib.git#20aff95cd80c141f80bdbf66895409a0046d2c2f
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0

Expand Down
303 changes: 303 additions & 0 deletions wled00/src/dependencies/espnow_wled/espnow_wled.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
#include "wled.h" // includes espnow_wled.h
/*
* Lightweight ESP-NOW driver for WLED
* by @dedehai (2026) licensed under EUPL 1.2 license (same as WLED)
* note: currently supports only broadcast sending, callback kept compatible with quickEspNow
*/

#ifndef WLED_DISABLE_ESPNOW

WledEspNow espNow;
//WledEspNowBroadcast espnowBroadcast; // note: WledEspNowBroadcast was added using AI with the goal of enabling porting the WLEDtubes usermod but I did not investigate if this is viable or useful so commented out for now

static const uint8_t BCAST[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // static broadcast MAC

// =========================================================================
// 802.11 Action frame layout for ESP-NOW — used to walk backwards from the payload pointer to reach the wifi_pkt_rx_ctrl_t (which carries RSSI).
// Reference: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_now.html
typedef struct {
uint16_t frame_head;
uint16_t duration;
uint8_t destination_address[6];
uint8_t source_address[6];
uint8_t broadcast_address[6];
uint16_t sequence_control;
uint8_t category_code;
uint8_t organization_identifier[3]; // 0x18fe34
uint8_t random_values[4];
struct {
uint8_t element_id; // 0xdd
uint8_t length;
uint8_t organization_identifier[3]; // 0x18fe34
uint8_t type; // 4
uint8_t version;
uint8_t body[0];
} vendor_specific_content;
} __attribute__((packed)) espnow_frame_format_t;

// =========================================================================
// Platform-specific SDK callbacks
// =========================================================================

#ifdef ARDUINO_ARCH_ESP32

// ----- ESP32 sent callback -----------------------------------------------
static void _espnowSentCB(const uint8_t *mac, esp_now_send_status_t status) {
if (espNow._inFlight > 0) espNow._inFlight--;
if (espNow._sentCB)
espNow._sentCB(const_cast<uint8_t*>(mac), (uint8_t)status);
}

// ----- ESP32 recv callback -----------------------------------------------
// Signature changed in IDF 5.0: the first parameter became esp_now_recv_info_t*
// which carries the source address, destination address (useful to detect
// broadcast) and the rx_ctrl struct with RSSI.
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
// note: IDF V5 code is AI generated, unreviewed and untested
static void _espnowRecvCB(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
if (!info || !data || len <= 0) return;
const uint8_t *mac = info->src_addr;
// rx_ctrl is a pointer to wifi_pkt_rx_ctrl_t; cast to int8_t to get signed RSSI.
int8_t rssi = (info->rx_ctrl) ? (int8_t)info->rx_ctrl->rssi : 0;
// Broadcast when the destination address has all bits set.
bool isBroadcast = (info->des_addr && memcmp(info->des_addr, BCAST, 6) == 0);

//espnowBroadcast.dispatch(mac, data, (uint8_t)len, rssi);
if (espNow._rcvdCB)
espNow._rcvdCB(const_cast<uint8_t*>(mac), const_cast<uint8_t*>(data), (uint8_t)len, (signed int)rssi, isBroadcast);
}

#else // IDF < 5.0

static void _espnowRecvCB(const uint8_t *mac, const uint8_t *data, int len) {
if (!mac || !data || len <= 0) return;
// Walk back through the WiFi frame buffer to reach wifi_pkt_rx_ctrl_t to get RSSI. Reference: https://github.com/gmag11/QuickESPNow
const espnow_frame_format_t *espnow_data = (const espnow_frame_format_t *)(data - sizeof(espnow_frame_format_t));
const wifi_promiscuous_pkt_t *promiscuous_pkt = (const wifi_promiscuous_pkt_t *)(data - sizeof(wifi_pkt_rx_ctrl_t) - sizeof(espnow_frame_format_t));
const wifi_pkt_rx_ctrl_t *rx_ctrl = &promiscuous_pkt->rx_ctrl;
int8_t rssi = (int8_t)rx_ctrl->rssi;
bool isBroadcast = (memcmp(espnow_data->destination_address, BCAST, 6) == 0);
//espnowBroadcast.dispatch(mac, data, (uint8_t)len, rssi);
if (espNow._rcvdCB)
espNow._rcvdCB(const_cast<uint8_t*>(mac), const_cast<uint8_t*>(data), (uint8_t)len, (signed int)rssi, isBroadcast);
}

#endif // ESP_IDF_VERSION

#else // ESP8266

// define wifi_pkt_rx_ctrl_t to match the hardware layout so we can extract RSSI
// https://github.com/espressif/ESP8266_RTOS_SDK/blob/master/components/esp8266/include/esp_wifi_types.h

typedef struct {
signed rssi: 8; /**< signal intensity of packet */
unsigned rate: 4; /**< data rate */
unsigned is_group: 1; /**< usually not used */
unsigned : 1; /**< reserve */
unsigned sig_mode: 2; /**< 0:is not 11n packet; 1:is 11n packet */
unsigned legacy_length: 12; /**< Length of 11bg mode packet */
unsigned damatch0: 1; /**< usually not used */
unsigned damatch1: 1; /**< usually not used */
unsigned bssidmatch0: 1; /**< usually not used */
unsigned bssidmatch1: 1; /**< usually not used */
unsigned mcs: 7; /**< if is 11n packet, shows the modulation(range from 0 to 76) */
unsigned cwb: 1; /**< if is 11n packet, shows if is HT40 packet or not */
unsigned HT_length: 16; /**< Length of 11n mode packet */
unsigned smoothing: 1; /**< reserve */
unsigned not_sounding: 1; /**< reserve */
unsigned : 1; /**< reserve */
unsigned aggregation: 1; /**< Aggregation */
unsigned stbc: 2; /**< STBC */
unsigned fec_coding: 1; /**< Flag is set for 11n packets which are LDPC */
unsigned sgi: 1; /**< SGI */
unsigned rxend_state: 8; /**< usually not used */
unsigned ampdu_cnt: 8; /**< ampdu cnt */
unsigned channel: 4; /**< which channel this packet in */
unsigned : 4; /**< reserve */
signed noise_floor: 8; /**< usually not used */
} wifi_pkt_rx_ctrl_t;

typedef struct {
wifi_pkt_rx_ctrl_t rx_ctrl;
uint8_t payload[0]; /* ieee80211 packet buff */
} wifi_promiscuous_pkt_t;


// ----- ESP8266 sent callback ---------------------------------------------
static void _espnowSentCB(uint8_t *mac, uint8_t status) {
if (espNow._inFlight > 0) espNow._inFlight--;
if (espNow._sentCB)
espNow._sentCB(mac, status);
}

// ----- ESP8266 recv callback ---------------------------------------------
static void _espnowRecvCB(uint8_t *mac, uint8_t *data, uint8_t len) {
if (!mac || !data || len == 0) return;
// Walk back through the WiFi frame buffer to reach the rx control header to get RSSI.
const espnow_frame_format_t *espnow_data = (const espnow_frame_format_t *)(data - sizeof(espnow_frame_format_t));
bool isBroadcast = (memcmp(espnow_data->destination_address, BCAST, 6) == 0);
const wifi_promiscuous_pkt_t *promiscuous_pkt = (const wifi_promiscuous_pkt_t *)(data - sizeof(wifi_pkt_rx_ctrl_t) - sizeof(espnow_frame_format_t));
const wifi_pkt_rx_ctrl_t *rx_ctrl = &promiscuous_pkt->rx_ctrl;
int8_t rssi = (int8_t)(rx_ctrl->rssi - 100); // ESP8266: raw RSSI is offset by ~+100 dBm vs actual signal strength
//espnowBroadcast.dispatch(mac, data, len, rssi);
if (espNow._rcvdCB)
espNow._rcvdCB(mac, data, len, (signed int)rssi, isBroadcast);
}

#endif

bool WledEspNow::begin(uint8_t channel, uint8_t iface) {
if (_running) stop(); // clean up before re-init

// note: channel must be 0-14 (14: used in Japan only), channel = 0 means "use current WiFi channel" (both on ESP8266 and ESP32, in AP and STA mode)
#ifdef ARDUINO_ARCH_ESP32
if (esp_now_init() != ESP_OK) {
DEBUG_PRINTLN(F("ESP-NOW esp_now_init() failed"));
return false;
}
if (esp_now_register_recv_cb(_espnowRecvCB) != ESP_OK) {
esp_now_deinit();
return false;
}
if (esp_now_register_send_cb(_espnowSentCB) != ESP_OK) {
esp_now_unregister_recv_cb();
esp_now_deinit();
return false;
}
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, BCAST, 6);
peer.channel = channel;
peer.ifidx = (wifi_interface_t)iface;
peer.encrypt = false;
if (esp_now_add_peer(&peer) != ESP_OK) {
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
esp_now_deinit();
return false;
}
_running = true;
return true;

#else // ESP8266
if (esp_now_init() != 0) {
DEBUG_PRINTLN(F("ESP-NOW esp_now_init() failed"));
return false;
}
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;
Comment on lines +186 to +190
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.

return true;
#endif
}

// STA mode: derives the channel from the current WiFi connection (channel 0 means "use current channel" for both ESP32 and ESP8266).
bool WledEspNow::begin() {
return begin(0, WIFI_IF_STA);
}

void WledEspNow::stop() {
if (!_running) return;
_running = false;
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
_inFlight = 0; // reset after unregistering callbacks
esp_now_del_peer(const_cast<uint8_t*>(BCAST));
esp_now_deinit(); // esp_now_deinit() frees any pending TX buffers
}

uint8_t WledEspNow::send(const uint8_t * /*addr*/, const uint8_t *data, uint8_t len) {
static bool isretransmit = false;
int err = 1; // default to error
// addr is ignored — we only support broadcast.
// len must be < ESP_NOW_MAX_DATA_LEN (250 bytes).
if (!_running) return err;
// ESP8266 SDK uses non-const uint8_t* parameters; const_cast is safe here.
if ( _inFlight < ESPNOW_MAX_INFLIGHT) {
err = esp_now_send(const_cast<uint8_t*>(BCAST), const_cast<uint8_t*>(data), len);
}
if (err == 0) _inFlight++; // ESP_OK == 0 on both platforms
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.
}
Comment on lines +221 to +228
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.

}
if (err != 0 && isretransmit) Serial.printf("ESP-NOW send failed with error %d, inflight=%d\n", err, (int)espNow._inFlight);
// TODO: should monitor somehow if sending fails repeatedly and do something about it
isretransmit = false; // reset flag
return err;
}

#ifdef ARDUINO_ARCH_ESP32
void WledEspNow::setWiFiBandwidth(uint8_t iface, uint8_t bw) {
esp_wifi_set_bandwidth((wifi_interface_t)iface, (wifi_bandwidth_t)bw);
}
#endif

/*
// start AI code, unreviewed, untested
// =========================================================================
// WledEspNowBroadcast — implementation
// =========================================================================

bool WledEspNowBroadcast::send(const uint8_t *msg, size_t len) {
if (len > 250) return false; // ESP-NOW max payload
return espNow.send(BCAST, msg, (uint8_t)len) == 0;
}

WledEspNowBroadcast::STATE WledEspNowBroadcast::getState() const {
switch (statusESPNow) {
case ESP_NOW_STATE_ON: return STARTED;
case ESP_NOW_STATE_UNINIT: // fall through
case ESP_NOW_STATE_ERROR:
default: return STOPPED;
}
}

bool WledEspNowBroadcast::registerCallback(receive_callback_t cb) {
for (size_t i = 0; i < WLED_ESPNOW_MAX_REGISTERED_CALLBACKS - 1; i++) {
if (_callbacks[i] == cb) return true; // already registered
if (_callbacks[i] == nullptr) {
_callbacks[i] = cb;
return true;
}
}
return false; // array full
}

bool WledEspNowBroadcast::removeCallback(receive_callback_t cb) {
size_t found = WLED_ESPNOW_MAX_REGISTERED_CALLBACKS;
for (size_t i = 0; i < WLED_ESPNOW_MAX_REGISTERED_CALLBACKS - 1; i++) {
if (_callbacks[i] == cb) { found = i; break; }
}
if (found == WLED_ESPNOW_MAX_REGISTERED_CALLBACKS) return false;
// Shift remaining entries left to close the gap.
for (size_t i = found; i < WLED_ESPNOW_MAX_REGISTERED_CALLBACKS - 1; i++)
_callbacks[i] = _callbacks[i + 1];
_callbacks[WLED_ESPNOW_MAX_REGISTERED_CALLBACKS - 1] = nullptr;
return true;
}

WledEspNowBroadcast::receive_filter_t
WledEspNowBroadcast::registerFilter(receive_filter_t filter) {
receive_filter_t old = _filter;
_filter = filter;
return old;
}

void WledEspNowBroadcast::dispatch(const uint8_t *mac, const uint8_t *data,
uint8_t len, int8_t rssi) {
if (_filter && !_filter(mac, data, len, rssi)) return;
for (size_t i = 0; i < WLED_ESPNOW_MAX_REGISTERED_CALLBACKS - 1; i++) {
if (_callbacks[i]) _callbacks[i](mac, data, len, rssi);
}
}
// end AI code, unreviewed, untested
*/

#endif // WLED_DISABLE_ESPNOW
Loading
Loading