-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Replace QuickESPNow with local library #5624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DedeHai
wants to merge
8
commits into
wled:main
Choose a base branch
from
DedeHai:ESPNow_nolib
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
af46182
initial version, fully reviewed but untested
DedeHai d508613
add support for missing RSSI in IDF V4
DedeHai 79767f8
optimize: make sending more robust
DedeHai 32901fb
add note
DedeHai 51ce7cc
bugfix
DedeHai 20eb5fb
add namespace, move to lib\
DedeHai d2d45fa
volatile -> atomic plus some comment cleanup
DedeHai 78e4ebd
fix & simplify sending function, fix for ESP8266
DedeHai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| 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. | ||
| } | ||
| } | ||
| 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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.