ESP-NOW & BLE Coexistence On ESP32: RX Failure
Hey guys! I'm running into a frustrating issue trying to get ESP-NOW and BLE to play nicely together on an ESP32. I've been experimenting with ESP-IDF 5.5.1 using NimBLE for Bluetooth, and I'm hitting a wall when it comes to true coexistence. While both protocols work perfectly fine on their own, the moment I try to run them simultaneously, ESP-NOW reception completely dies. Let's dive into the problem, the code, and hopefully, find a solution together.
The Setup: ESP-NOW and BLE Working Independently
My goal is to have an ESP32 that can listen for ESP-NOW packets (specifically, location data) and, at the same time, scan for BLE advertisements (like iBeacons, providing RSSI data). The idea is to calculate distances between these devices and report the information. Sounds simple, right? Well, not quite.
The Isolated Successes
-
ESP-NOW Alone (Works Perfectly): When I run just ESP-NOW, my ESP32 receives all packets without any issues. The callback function
espnow_recv_cbis triggered every time a packet arrives. I can send test packets and see the reception count increasing in the serial monitor. This proves the ESP-NOW setup itself is correct. -
BLE Alone (Works Perfectly): Similarly, if I run just the BLE code, my ESP32 successfully scans and detects BLE advertisements. The
ble_gap_eventcallback gets hit, and I see the advertisement data, including RSSI values, in the serial output. This verifies that the BLE scanning is also working as expected.
So, both technologies function flawlessly in isolation. But here is the problem.
The Problem: ESP-NOW RX Failure with BLE Active
Hereβs where things get sticky. The moment I enable BLE scanning alongside ESP-NOW, my ESP-NOW receiver stops working completely. The espnow_recv_cb callback is never called, and no packets are received, even though they are being sent from a separate ESP32 on the same channel. This is the core issue.
Workaround (Not a Real Solution)
I've found a workaround that technically allows both technologies to be used, but it's not a true solution because it defeats the purpose of simultaneous operation.
- Sequential Operation: I can switch between WiFi (ESP-NOW) and BLE by completely stopping one before starting the other. For example: First, use WiFi to send and receive ESP-NOW data, then turn WiFi off. After, turn on BLE to scan for advertisements, then turn BLE off. Repeating this process. With this method, ESP-NOW and BLE work perfectly separately, but they are not used simultaneously. This proves the hardware is fine but defeats the purpose of trying to run both at the same time, which is very inefficient.
Code Examples: Demonstrating the Problem
I've created minimal, reproducible examples (MREs) to clearly demonstrate the issue. These snippets should help you understand the problem and potentially replicate it on your setup.
Example 1: ESP-NOW Only (WORKS β )
Here's the code for the ESP-NOW-only setup. This is a basic example to verify that ESP-NOW itself is functioning correctly. As you'll see, it receives packets continuously.
#include <esp_now.h>
#include <esp_wifi.h>
#include <esp_event.h>
#include <nvs_flash.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
static int rx_count = 0;
void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
rx_count++;
printf("β
ESP-NOW RX #%d: %d bytes from %02x:%02x:%02x:%02x:%02x:%02x\n",
rx_count, len,
info->src_addr[0], info->src_addr[1], info->src_addr[2],
info->src_addr[3], info->src_addr[4], info->src_addr[5]);
}
void espnow_send_cb(const esp_now_send_info_t *info, esp_now_send_status_t status) {
printf("ESP-NOW TX: %s\n", status == ESP_NOW_SEND_SUCCESS ? "OK" : "FAIL");
}
extern "C" void app_main() {
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_storage(WIFI_STORAGE_RAM);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_ps(WIFI_PS_NONE);
esp_now_init();
esp_now_register_recv_cb(espnow_recv_cb);
esp_now_register_send_cb(espnow_send_cb);
// Add broadcast peer
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, (uint8_t[]){0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}, 6);
peer.channel = 1;
peer.ifidx = WIFI_IF_STA;
esp_now_add_peer(&peer);
printf("ESP-NOW only mode - waiting for packets...\n");
// Send test packets every 1s
uint8_t test_data[] = {0x11, 0x22, 0x33, 0x44};
while(1) {
esp_now_send(peer.peer_addr, test_data, sizeof(test_data));
vTaskDelay(pdMS_TO_TICKS(1000));
printf("Status: %d packets received\n", rx_count);
}
}
Result: As expected, the code continuously receives packets, and the callback function is triggered with each successful reception.
Example 2: BLE Only (WORKS β )
This code snippet demonstrates the BLE scanning functionality. It scans for advertisements and prints the advertisement data to the serial monitor.
#include <nvs_flash.h>
#include <nimble/nimble_port.h>
#include <nimble/nimble_port_freertos.h>
#include <host/ble_hs.h>
#include <host/ble_gap.h>
#include <services/gap/ble_svc_gap.h>
#include <services/gatt/ble_svc_gatt.h>
static int ad_count = 0;
int ble_gap_event(struct ble_gap_event *event, void *arg) {
if (event->type == BLE_GAP_EVENT_DISC) {
ad_count++;
if (ad_count <= 10 || ad_count % 10 == 0) {
printf("β
BLE Ad #%d: %02x:%02x:%02x:%02x:%02x:%02x RSSI=%d\n",
ad_count,
event->disc.addr.val[5], event->disc.addr.val[4],
event->disc.addr.val[3], event->disc.addr.val[2],
event->disc.addr.val[1], event->disc.addr.val[0],
event->disc.rssi);
}
} else if (event->type == BLE_GAP_EVENT_DISC_COMPLETE) {
printf("BLE scan complete, restarting...\n");
struct ble_gap_disc_params params = {};
params.passive = 1;
params.filter_duplicates = 1;
params.itvl = 0x10;
params.window = 0x10;
ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, ¶ms, ble_gap_event, NULL);
}
return 0;
}
void ble_on_sync() {
printf("BLE synced, starting scan...\n");
struct ble_gap_disc_params params = {};
params.passive = 1;
params.filter_duplicates = 1;
params.itvl = 0x10;
params.window = 0x10;
ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, ¶ms, ble_gap_event, NULL);
}
void ble_host_task(void *param) {
nimble_port_run();
}
extern "C" void app_main() {
nvs_flash_init();
nimble_port_init();
ble_hs_cfg.sync_cb = ble_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
ble_svc_gap_init();
ble_svc_gatt_init();
nimble_port_freertos_init(ble_host_task);
printf("BLE only mode - scanning...\n");
while(1) {
vTaskDelay(pdMS_TO_TICKS(10000));
printf("Status: %d advertisements seen\n", ad_count);
}
}
Result: This code successfully detects and prints BLE advertisements, confirming that the BLE scanning is functioning correctly.
Example 3: ESP-NOW + BLE Together (FAILS β)
This is where the problem manifests. This code attempts to run ESP-NOW and BLE simultaneously. As you'll see, the ESP-NOW RX count stays at zero, indicating that no packets are being received.
#include <esp_now.h>
#include <esp_wifi.h>
#include <esp_event.h>
#include <nvs_flash.h>
#include <nimble/nimble_port.h>
#include <nimble/nimble_port_freertos.h>
#include <host/ble_hs.h>
#include <host/ble_gap.h>
#include <services/gap/ble_svc_gap.h>
#include <services/gatt/ble_svc_gatt.h>
static int espnow_rx_count = 0;
static int ble_ad_count = 0;
// ESP-NOW callbacks
void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
espnow_rx_count++;
printf("π₯ ESP-NOW RX #%d: %d bytes\n", espnow_rx_count, len);
}
void espnow_send_cb(const esp_now_send_info_t *info, esp_now_send_status_t status) {
// Send callback works fine
}
// BLE callbacks
int ble_gap_event(struct ble_gap_event *event, void *arg) {
if (event->type == BLE_GAP_EVENT_DISC) {
ble_ad_count++;
if (ble_ad_count % 10 == 0) {
printf("π± BLE Ad #%d\n", ble_ad_count);
}
} else if (event->type == BLE_GAP_EVENT_DISC_COMPLETE) {
struct ble_gap_disc_params params = {};
params.passive = 1;
params.filter_duplicates = 1;
params.itvl = 0x10;
params.window = 0x10;
ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, ¶ms, ble_gap_event, NULL);
}
return 0;
}
void ble_on_sync() {
struct ble_gap_disc_params params = {};
params.passive = 1;
params.filter_duplicates = 1;
params.itvl = 0x10;
params.window = 0x10;
ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, ¶ms, ble_gap_event, NULL);
}
void ble_host_task(void *param) {
nimble_port_run();
}
extern "C" void app_main() {
nvs_flash_init();
// Init WiFi for ESP-NOW
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_storage(WIFI_STORAGE_RAM);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_ps(WIFI_PS_NONE);
esp_now_init();
esp_now_register_recv_cb(espnow_recv_cb);
esp_now_register_send_cb(espnow_send_cb);
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, (uint8_t[]){0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}, 6);
peer.channel = 1;
peer.ifidx = WIFI_IF_STA;
esp_now_add_peer(&peer);
// Init BLE
nimble_port_init();
ble_hs_cfg.sync_cb = ble_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
ble_svc_gap_init();
ble_svc_gatt_init();
nimble_port_freertos_init(ble_host_task);
printf("ESP-NOW + BLE coexistence mode\n");
// Send ESP-NOW packets every 1s
uint8_t test_data[] = {0xAA, 0xBB, 0xCC, 0xDD};
while(1) {
esp_now_send(peer.peer_addr, test_data, sizeof(test_data));
vTaskDelay(pdMS_TO_TICKS(1000));
printf("Status: ESP-NOW RX=%d (β ZERO!), BLE Ads=%d (β
Works)\n",
espnow_rx_count, ble_ad_count);
}
}
Result: ESP-NOW RX count remains zero, confirming the RX failure. However, the ESP-NOW TX callback confirms that packets are sent, and the receiving device confirms reception, confirming this is not a sending problem.
Things I've Tried (Without Success)
I've already exhausted several troubleshooting steps in an attempt to resolve this issue. Here's a rundown of the things I've tried. Hopefully, this helps avoid redundant suggestions.
- Adjusting BLE Scan Parameters: I experimented with various scan intervals and windows (e.g., 10ms, 50ms, 100ms, and 1280ms) for BLE scanning, hoping to find a combination that would allow ESP-NOW to function. Unfortunately, these changes didn't make any difference.
- WiFi Power Saving Modes: I tested different WiFi power-saving modes (NONE, MIN_MODEM, and MAX_MODEM) to see if they were interfering with ESP-NOW reception. None of these settings resolved the problem.
- Coexistence Preference API: Tried setting the coexistence preference using the deprecated API:
esp_coex_preference_set(ESP_COEX_PREFER_WIFI). This didn't change the behavior. - WiFi Channel: I tested different WiFi channels (1, 6, and 11) to avoid potential interference, but the ESP-NOW RX failure persisted.
- ESP-NOW Wake Window: I tried setting the ESP-NOW wake window using
esp_now_set_wake_window(65535), but this also didn't help. - Task Priorities: I experimented with different FreeRTOS task priorities for both ESP-NOW and BLE tasks, hoping to influence the scheduling and improve coexistence. However, this also didn't solve the issue.
The Expected Behavior vs. Reality
According to the ESP32 documentation and the coexistence matrix provided by Espressif, WiFi/ESP-NOW in STA mode combined with BLE should be supported, with automatic coexistence. In the documentation, ESP-NOW RX is marked as