Chapter 70: ESP-NOW Protocol for Direct Device Communication
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamental principles and architecture of the ESP-NOW protocol.
- Initialize ESP-NOW and the underlying Wi-Fi stack on ESP32 devices.
- Manage ESP-NOW peer devices, including adding, modifying, and deleting them.
- Send data to specific peers or broadcast to all registered peers.
- Receive data from ESP-NOW peers and process it.
- Implement and utilize send and receive callback functions to manage data transmission status and incoming data.
- Configure and use ESP-NOW’s optional encryption features for secure communication.
- Identify suitable use cases for ESP-NOW and recognize its advantages and limitations.
Introduction
In the realm of IoT and device-to-device communication, there’s often a need for simple, fast, and low-overhead data exchange without the complexities of traditional network stacks like TCP/IP or the connection-oriented nature of Bluetooth profiles. Espressif Systems introduced ESP-NOW to address this need—a proprietary, connectionless communication protocol that enables direct, low-latency communication between ESP32 devices (and other Espressif chips like ESP8266).
ESP-NOW leverages the 2.4 GHz Wi-Fi hardware but operates at a layer below the standard Wi-Fi networking protocols. This allows for quick data transmission with minimal setup, making it ideal for applications like remote controls, sensor data aggregation from nearby devices, simple device triggers, and low-latency control systems where responsiveness is key. Imagine a wireless light switch directly controlling a light bulb, or multiple sensors sending quick status updates to a central ESP32 hub—all without needing a Wi-Fi access point or a complex Bluetooth pairing process.
This chapter will guide you through the intricacies of the ESP-NOW protocol. You’ll learn how to initialize it, manage communication peers, send and receive data, and implement security. We will explore practical examples to solidify your understanding and enable you to build efficient direct communication solutions with ESP32.
Theory
Core Concepts of ESP-NOW
ESP-NOW is a fast, connectionless communication protocol defined by Espressif. Its key characteristics include:
- Connectionless: Unlike Wi-Fi stations connecting to an Access Point (AP) or Bluetooth devices forming persistent connections, ESP-NOW does not require a formal connection handshake before data transmission. Once peers are registered, data can be sent directly.
- Direct Device-to-Device: It enables communication directly between ESP devices without an intermediary like a router or AP for the ESP-NOW traffic itself (though Wi-Fi must be initialized).
- MAC Address-Based: Devices are identified and addressed using their unique Wi-Fi MAC addresses.
- Short Packets: Optimized for transmitting small packets of data, up to a maximum of 250 bytes per message.
- Low Latency: Due to its connectionless nature and minimal overhead, ESP-NOW offers very low latency, often in the millisecond range.
- Acknowledgment (ACK): Transmissions are acknowledged at the 802.11 MAC layer. The sender receives a status callback indicating whether the transmission was successful (ACK received) or failed.
- Unicast and Broadcast:
- Unicast: Sending data to a specific, registered peer device.
- Broadcast: Sending data to all registered peer devices that use the broadcast MAC address (
FF:FF:FF:FF:FF:FF
). Note that for a device to receive broadcast ESP-NOW messages, it must have added the sender with the broadcast MAC address as a peer, or more commonly, the sender adds peers with their specific MACs and sends to the broadcast MAC. True promiscuous broadcast reception is not the primary mode; it’s more about sending to a group of known (or specially configured) peers. A more typical broadcast scenario involves the sender registering multiple peers and then sending a single message to the broadcast MAC address, which is then received by all its registered peers that are listening.
- Uses Wi-Fi Radio: ESP-NOW utilizes the existing 2.4 GHz Wi-Fi radio hardware. This means Wi-Fi must be initialized (typically in station or softAP mode) for ESP-NOW to function, even if the ESP32 isn’t connected to an AP for internet access.
- Shared Wi-Fi Channel: All ESP-NOW devices intending to communicate with each other must be operating on the same Wi-Fi channel.
Characteristic | Description | Implication / Benefit |
---|---|---|
Connectionless | No formal connection handshake required before data transmission. | Low latency, quick data exchange, reduced overhead. |
Direct Device-to-Device | Enables communication directly between ESP devices without an intermediary router for ESP-NOW traffic. | Simple network topology, operates independently of Wi-Fi network availability (once Wi-Fi is initialized on devices). |
MAC Address-Based | Devices are identified and addressed using their unique Wi-Fi MAC addresses. | Clear and unique addressing for peers. |
Short Packets | Optimized for transmitting small packets of data (up to 250 bytes per message). | Efficient for sensor readings, control commands, status updates. |
Acknowledgment (ACK) | Transmissions are acknowledged at the 802.11 MAC layer; sender receives a status callback. | Provides reliability by confirming message delivery. |
Unicast & Broadcast | Supports sending to a specific peer (unicast) or to all registered/broadcast-configured peers. | Flexible communication patterns for one-to-one or one-to-many scenarios. |
Uses Wi-Fi Radio | Leverages the existing 2.4 GHz Wi-Fi radio hardware. | No extra radio hardware needed if Wi-Fi is already present. Wi-Fi must be initialized. |
Shared Wi-Fi Channel | Communicating ESP-NOW devices must be on the same Wi-Fi channel. | Requires channel coordination for successful communication. |
Optional Encryption | Supports AES-CCMP encryption for unicast messages using a shared Local Master Key (LMK). | Enables secure data exchange between specific peers. |
ESP-NOW Initialization
Before using ESP-NOW, the Wi-Fi subsystem must be initialized. Typically, this involves:
%%{init: {"theme": "base", "themeVariables": { "fontFamily": "Open Sans"}}}%% graph TD Start("Start: app_main") --> NVS["Initialize NVS <br> <span style='font-family:monospace; font-size:0.9em;'>nvs_flash_init()</span>"]; NVS --> NetIF["Initialize TCP/IP Adapter <br> <span style='font-family:monospace; font-size:0.9em;'>esp_netif_init()</span>"]; NetIF --> EventLoop["Create Default Event Loop <br> <span style='font-family:monospace; font-size:0.9em;'>esp_event_loop_create_default()</span>"]; EventLoop --> WiFiInitCfg["Create Default Wi-Fi Config <br> <span style='font-family:monospace; font-size:0.9em;'>WIFI_INIT_CONFIG_DEFAULT()</span>"]; WiFiInitCfg --> WiFiInit["Initialize Wi-Fi <br> <span style='font-family:monospace; font-size:0.9em;'>esp_wifi_init()</span>"]; WiFiInit --> WiFiStorage["Set Wi-Fi Storage (Optional) <br> <span style='font-family:monospace; font-size:0.9em;'>esp_wifi_set_storage(WIFI_STORAGE_RAM)</span>"]; WiFiStorage --> WiFiMode["Set Wi-Fi Mode (STA/AP) <br> <span style='font-family:monospace; font-size:0.9em;'>esp_wifi_set_mode()</span>"]; WiFiMode --> WiFiStart{"Start Wi-Fi? <br> <span style='font-family:monospace; font-size:0.9em;'>esp_wifi_start()</span>"}; WiFiStart -- Success --> SetChannel["Set Wi-Fi Channel (Optional but Recommended) <br> <span style='font-family:monospace; font-size:0.9em;'>esp_wifi_set_channel()</span>"]; SetChannel --> EspNowInit{"Initialize ESP-NOW? <br> <span style='font-family:monospace; font-size:0.9em;'>esp_now_init()</span>"}; EspNowInit -- Success --> RegSendCB["Register Send Callback <br> <span style='font-family:monospace; font-size:0.9em;'>esp_now_register_send_cb()</span>"]; RegSendCB --> RegRecvCB["Register Receive Callback <br> <span style='font-family:monospace; font-size:0.9em;'>esp_now_register_recv_cb()</span>"]; RegRecvCB --> Ready("ESP-NOW Ready"); WiFiStart -- Failure --> ErrorWiFi["Handle Wi-Fi Start Error"]; EspNowInit -- Failure --> ErrorEspNow["Handle ESP-NOW Init Error"]; ErrorWiFi --> End("End"); ErrorEspNow --> End("End"); classDef startEnd fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; class Start,Ready startEnd; class NVS,NetIF,EventLoop,WiFiInitCfg,WiFiInit,WiFiStorage,WiFiMode,SetChannel,RegSendCB,RegRecvCB process; class WiFiStart,EspNowInit decision; class ErrorWiFi,ErrorEspNow,End error;
- Initializing NVS (Non-Volatile Storage), as Wi-Fi calibration data might be stored there.
- Initializing the TCP/IP adapter stack (
esp_netif_init()
). - Creating a default event loop (
esp_event_loop_create_default()
). - Initializing Wi-Fi with default configuration (
wifi_init_config_t
). - Setting Wi-Fi mode to station (
WIFI_MODE_STA
), softAP (WIFI_MODE_AP
), or APSTA (WIFI_MODE_APSTA
). Station mode is common even if not connecting to an AP. - Starting Wi-Fi (
esp_wifi_start()
). - Initializing ESP-NOW using
esp_now_init()
. - Deinitializing ESP-NOW using
esp_now_deinit()
when done.
Peer Management
ESP-NOW communication relies on a list of registered peer devices. Each peer entry contains information about the device to communicate with.
- Peer Information (
esp_now_peer_info_t
):peer_addr
: MAC address of the peer device.channel
: Wi-Fi channel the peer is on (0 for current channel). If non-zero, the ESP32 will switch to this channel to send data to this specific peer ififidx
allows.ifidx
: Wi-Fi interface (WIFI_IF_STA
orWIFI_IF_AP
) to use for sending data to this peer.encrypt
: Boolean, true if communication with this peer should be encrypted.lmk[ESP_NOW_KEY_LEN]
: Local Master Key (LMK) for encrypted communication. Must be the same on both peers.
- Adding a Peer:
esp_now_add_peer(const esp_now_peer_info_t *peer_info)
- Registers a device in the peer list.
- A device can be a unicast peer (specific MAC) or a broadcast peer (using broadcast MAC
FF:FF:FF:FF:FF:FF
).
- Deleting a Peer:
esp_now_del_peer(const uint8_t *peer_addr)
- Modifying a Peer:
esp_now_mod_peer(const esp_now_peer_info_t *peer_info)
- Getting Peer Information:
esp_now_get_peer(const uint8_t *peer_addr, esp_now_peer_info_t *peer_info)
- Checking Peer Existence:
esp_now_is_peer_exist(const uint8_t *peer_addr)
- Peer List Limits: The number of peers that can be registered is limited (e.g., up to 20 unencrypted peers, fewer if encryption is used for some). Check
CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM
for encrypted peer limits.
%%{init: {"theme": "base", "themeVariables": { "fontFamily": "Open Sans"}}}%% graph LR subgraph Application Logic A1["Need to send to New Peer X"] --> AddPeer; A2["Need to update Peer X's LMK/Channel"] --> ModPeer; A3["Peer X no longer needed"] --> DelPeer; A4["Check if Peer X exists?"] --> CheckPeer; A5["Get Peer X's current info?"] --> GetPeerInfo; end subgraph "ESP-NOW API Calls" AddPeer["esp_now_add_peer(peer_X_info)"] --> R1{"Result: Success/Fail"}; ModPeer["esp_now_mod_peer(new_peer_X_info)"] --> R2{"Result: Success/Fail"}; DelPeer["esp_now_del_peer(peer_X_mac)"] --> R3{"Result: Success/Fail"}; CheckPeer["esp_now_is_peer_exist(peer_X_mac)"] --> R4{"Result: True/False"}; GetPeerInfo["esp_now_get_peer(peer_X_mac, &out_info)"] --> R5{"Result: Success/Fail <br> (info in out_info)"}; end subgraph "ESP-NOW Internal Peer List" direction LR PL["Peer List Memory"]; R1 -- Success --> PL_Add["Peer X Added/Updated in List"]; R2 -- Success --> PL_Mod["Peer X Modified in List"]; R3 -- Success --> PL_Del["Peer X Removed from List"]; R4 -- True --> PL_Found["Peer X Found in List"]; R5 -- Success --> PL_Data["Peer X Info Retrieved"]; PL_Add --> PL; PL_Mod --> PL; PL_Del --> PL; PL_Found --> PL; PL_Data --> PL; end PL_Add -.-> A1_Confirm["Inform App: Peer Added"]; PL_Mod -.-> A2_Confirm["Inform App: Peer Modified"]; PL_Del -.-> A3_Confirm["Inform App: Peer Deleted"]; PL_Found -.-> A4_Confirm["Inform App: Peer Exists"]; PL_Data -.-> A5_Confirm["Inform App: Peer Info Ready"]; classDef appLogic fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef apiCall fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef peerList fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1px,color:#0369A1; classDef result fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46; classDef confirm fill:#F3E8FF,stroke:#7E22CE,stroke-width:1px,color:#6B21A8; class A1,A2,A3,A4,A5 appLogic; class AddPeer,ModPeer,DelPeer,CheckPeer,GetPeerInfo apiCall; class PL,PL_Add,PL_Mod,PL_Del,PL_Found,PL_Data peerList; class R1,R2,R3,R4,R5 result; class A1_Confirm,A2_Confirm,A3_Confirm,A4_Confirm,A5_Confirm confirm;
Function | Key Parameters / Structure | Purpose |
---|---|---|
esp_now_add_peer() | const esp_now_peer_info_t *peer_info | Registers a new device in the ESP-NOW peer list. |
esp_now_del_peer() | const uint8_t *peer_addr | Removes a device from the peer list using its MAC address. |
esp_now_mod_peer() | const esp_now_peer_info_t *peer_info | Modifies the information of an existing peer (e.g., channel, LMK, encryption status). |
esp_now_get_peer() | const uint8_t *peer_addr, esp_now_peer_info_t *peer_info | Retrieves information about a registered peer. |
esp_now_is_peer_exist() | const uint8_t *peer_addr | Checks if a peer with the given MAC address is already registered. |
esp_now_fetch_peer() | bool from_head, esp_now_peer_info_t *peer_info | Fetches peer information one by one from the list (useful for iterating). |
esp_now_get_peer_num() | esp_now_peer_num_t *num | Gets the total number of registered peers and number of encrypted peers. |
Key fields in esp_now_peer_info_t structure: | ||
uint8_t peer_addr[ESP_NOW_ETH_ALEN] | MAC address of the peer device. | |
uint8_t channel | Wi-Fi channel for the peer (0 for current channel). | |
wifi_if_t ifidx | Wi-Fi interface (WIFI_IF_STA or WIFI_IF_AP) to use for this peer. | |
bool encrypt | Set to true if communication with this peer should be encrypted. | |
uint8_t lmk[ESP_NOW_KEY_LEN] | Local Master Key (LMK) for encrypted communication (16 bytes). |
Data Transmission
Once peers are registered (for unicast) or ESP-NOW is initialized (for broadcast to registered broadcast peers):
esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len)
:peer_addr
: MAC address of the recipient. UseNULL
or the broadcast MAC address (FF:FF:FF:FF:FF:FF
) to send to all registered peers that match the broadcast criteria (this usually means sending to peers that were added with the broadcast MAC, or ifpeer_addr
isNULL
, it sends to all peers in its list).data
: Pointer to the data buffer.len
: Length of the data (max 250 bytes).- This function is non-blocking. It queues the data for transmission.
- Send Callback (
esp_now_register_send_cb(esp_now_send_cb_t cb)
):- Registers a callback function that is invoked after a data transmission attempt.
- Callback signature:
void send_callback_name(const uint8_t *mac_addr, esp_now_send_status_t status)
mac_addr
: MAC address of the peer the data was sent to.status
:ESP_NOW_SEND_SUCCESS
if ACK received,ESP_NOW_SEND_FAIL
otherwise.- This callback is crucial for confirming delivery or handling transmission failures.
Operation | Key Function / Callback | Parameters / Signature | Purpose & Key Info |
---|---|---|---|
Data Transmission | esp_now_send() | (const uint8_t *peer_addr, const uint8_t *data, size_t len) | Sends data to a specified peer (or broadcast if peer_addr is NULL/broadcast MAC). Max data length is 250 bytes. Non-blocking. |
Send Status Callback Registration | esp_now_register_send_cb() | (esp_now_send_cb_t cb) | Registers a function to be called after a send attempt. |
Send Status Callback Function | your_send_cb_name | (const uint8_t *mac_addr, esp_now_send_status_t status) | Invoked asynchronously. mac_addr is the peer. status is ESP_NOW_SEND_SUCCESS or ESP_NOW_SEND_FAIL. Crucial for delivery confirmation. |
Receive Data Callback Registration | esp_now_register_recv_cb() | (esp_now_recv_cb_t cb) | Registers a function to be called when ESP-NOW data is received. |
Receive Data Callback Function | your_recv_cb_name | (ESP-IDF v5.x+): (const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) (Pre ESP-IDF v5.0): (const uint8_t *mac_addr, const uint8_t *data, int len) |
Invoked when data arrives. recv_info->src_addr (or mac_addr pre-v5) is sender’s MAC. data points to received payload. len is payload length. recv_info->rx_ctrl contains RSSI. Copy data if needed for later use. |
Data Reception
- Receive Callback (
esp_now_register_recv_cb(esp_now_recv_cb_t cb)
):- Registers a callback function that is invoked when an ESP-NOW packet is received.
- Callback signature:
void recv_callback_name(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len)
- Prior to ESP-IDF v5.0, the signature was
void recv_callback_name(const uint8_t *mac_addr, const uint8_t *data, int len)
. Theesp_now_recv_info_t
structure now containssrc_addr
(sender’s MAC) andrx_ctrl
(RSSI and other PHY info).
- Prior to ESP-IDF v5.0, the signature was
recv_info->src_addr
: MAC address of the sender.data
: Pointer to the received data buffer.len
: Length of the received data.- The data buffer passed to the callback is often temporary; copy it if needed for later processing.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% sequenceDiagram; participant Sender as Device A (Sender); participant Receiver as Device B (Receiver); rect rgb(237, 233, 254, 0.3) Note over Sender: ESP-NOW & Wi-Fi Initialized Sender->>Sender: esp_now_init() Sender->>Sender: esp_now_register_send_cb(on_send_status) Sender->>Sender: esp_now_add_peer(Device_B_MAC, channel, ifidx, encrypt=false) end rect rgb(219, 234, 254, 0.3) Note over Receiver: ESP-NOW & Wi-Fi Initialized Receiver->>Receiver: esp_now_init() Receiver->>Receiver: esp_now_register_recv_cb(on_data_recv) Note over Receiver: (May also add Device A as peer if sending back) end Sender->>+Receiver: esp_now_send(Device_B_MAC, data, len) <br> [ESP-NOW Packet (Data)] Note right of Sender: Packet Queued for Tx activate Receiver; Note left of Receiver: Wi-Fi Radio receives packet Receiver->>Receiver: MAC Layer sends ACK to Sender Receiver-->>-Sender: [802.11 ACK] Receiver->>Receiver: on_data_recv(Device_A_MAC, data, len) is called deactivate Receiver; activate Sender; Note right of Sender: Sender's Wi-Fi Radio receives ACK Sender->>Sender: on_send_status(Device_B_MAC, ESP_NOW_SEND_SUCCESS) is called deactivate Sender; actor Sender; actor Receiver;
Wi-Fi Channel Management
ESP-NOW requires communicating devices to be on the same Wi-Fi channel.
- The primary Wi-Fi channel can be set using
esp_wifi_set_channel(uint8_t primary, wifi_second_chan_t second)
. - When adding a peer with
esp_now_add_peer()
, you can specify a channel for that peer. If thechannel
field inesp_now_peer_info_t
is non-zero, the ESP32 will attempt to switch to that channel for transmitting to that specific peer, provided the Wi-Fi interface (ifidx
) allows it. Ifchannel
is 0, the current primary channel of the interface is used. - For robust communication in environments with interference, more complex channel agreement or hopping schemes might be needed at the application layer, but ESP-NOW itself is channel-agnostic beyond this configuration.
Security: Encryption
ESP-NOW supports optional AES-CCMP encryption for unicast messages.
- To enable encryption for a peer, set
encrypt = true
inesp_now_peer_info_t
. - Provide a 16-byte Local Master Key (LMK) in the
lmk
field ofesp_now_peer_info_t
. - Both the sender and receiver must be configured with the exact same LMK for the specific peer pairing.
- The number of encrypted peers is limited by
CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM
(default is often around 6-7). - Broadcast messages are not encrypted by ESP-NOW.
Power Saving
Since ESP-NOW is connectionless, devices can remain in a low-power state and only wake the Wi-Fi radio when they need to send or are expecting data (if a coordinated wake-up scheme is implemented at the application level). ESP-NOW can work in conjunction with Wi-Fi power-saving modes. The send/receive callbacks allow the application to quickly process data and potentially return to sleep.
Practical Examples
Prerequisites:
- At least two ESP32 boards.
- VS Code with the Espressif IDF Extension.
- ESP-IDF v5.x.
Obtaining MAC Addresses:
You’ll need the MAC address of the peer ESP32(s). You can get an ESP32’s MAC address programmatically:
#include "esp_wifi.h"
// ...
uint8_t mac_addr[6];
esp_wifi_get_mac(ESP_IF_WIFI_STA, mac_addr); // Or ESP_IF_WIFI_AP
ESP_LOGI("MAC_INFO", "STA MAC: %02x:%02x:%02x:%02x:%02x:%02x",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
Flash a simple program to print the MAC address of each board and note them down.
Example 1: Simple ESP-NOW Sender
This ESP32 will periodically send a message to a specific peer.
Replace PEER_MAC_ADDRESS with the actual MAC address of your receiver ESP32.
Code (sender/main/esp_now_sender_main.c):
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "esp_now.h"
#define SENDER_TAG "ESP_NOW_SENDER"
// Replace with the MAC address of the receiver ESP32
static uint8_t s_peer_mac[ESP_NOW_ETH_ALEN] = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX};
#define WIFI_CHANNEL 1 // Ensure sender and receiver are on the same channel
// Send callback
static void app_esp_now_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) {
if (mac_addr == NULL) {
ESP_LOGE(SENDER_TAG, "Send CB mac_addr is NULL");
return;
}
ESP_LOGI(SENDER_TAG, "Send CB to " MACSTR ": %s", MAC2STR(mac_addr),
(status == ESP_NOW_SEND_SUCCESS) ? "Success" : "Fail");
}
static void wifi_init(void) {
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE));
// For ESP-NOW, we don't need to connect to an AP, but Wi-Fi must be started.
// If you need to connect to an AP for other purposes, do it here.
}
void sender_task(void *pvParameter) {
esp_now_peer_info_t peer_info = {0};
memcpy(peer_info.peer_addr, s_peer_mac, ESP_NOW_ETH_ALEN);
peer_info.channel = WIFI_CHANNEL; // Must be the same as receiver's channel
peer_info.ifidx = ESP_IF_WIFI_STA; // Interface to use
peer_info.encrypt = false; // No encryption for this example
if (esp_now_is_peer_exist(s_peer_mac)) {
ESP_LOGI(SENDER_TAG, "Peer " MACSTR " already exists, deleting old one.", MAC2STR(s_peer_mac));
ESP_ERROR_CHECK(esp_now_del_peer(s_peer_mac));
}
ESP_LOGI(SENDER_TAG, "Adding peer " MACSTR, MAC2STR(s_peer_mac));
ESP_ERROR_CHECK(esp_now_add_peer(&peer_info));
uint32_t count = 0;
char send_data[100];
while (true) {
sprintf(send_data, "Hello from Sender! Count: %" PRIu32, count++);
esp_err_t ret = esp_now_send(s_peer_mac, (uint8_t *)send_data, strlen(send_data));
if (ret == ESP_OK) {
ESP_LOGI(SENDER_TAG, "Sent: %s", send_data);
} else {
ESP_LOGE(SENDER_TAG, "Send error: %s", esp_err_to_name(ret));
}
vTaskDelay(pdMS_TO_TICKS(2000)); // Send every 2 seconds
}
}
void app_main(void) {
ESP_ERROR_CHECK(nvs_flash_init());
wifi_init();
ESP_LOGI(SENDER_TAG, "Initializing ESP-NOW...");
ESP_ERROR_CHECK(esp_now_init());
ESP_LOGI(SENDER_TAG, "Registering ESP-NOW send callback...");
ESP_ERROR_CHECK(esp_now_register_send_cb(app_esp_now_send_cb));
// Get and print this device's MAC address
uint8_t mac_addr[6];
esp_wifi_get_mac(ESP_IF_WIFI_STA, mac_addr);
ESP_LOGI(SENDER_TAG, "My STA MAC: " MACSTR, MAC2STR(mac_addr));
xTaskCreate(sender_task, "sender_task", 4096, NULL, 5, NULL);
}
Example 2: Simple ESP-NOW Receiver
This ESP32 will listen for ESP-NOW messages.
Code (receiver/main/esp_now_receiver_main.c):
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "esp_now.h"
#define RECEIVER_TAG "ESP_NOW_RECEIVER"
#define WIFI_CHANNEL 1 // Ensure sender and receiver are on the same channel
// Receive callback
// For ESP-IDF v5.x and later, the signature includes esp_now_recv_info_t
static void app_esp_now_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) {
if (recv_info == NULL || data == NULL || len <= 0) {
ESP_LOGE(RECEIVER_TAG, "Receive CB error");
return;
}
const uint8_t *mac_addr = recv_info->src_addr;
ESP_LOGI(RECEIVER_TAG, "Received %d bytes from " MACSTR ":", len, MAC2STR(mac_addr));
// Print data as string, ensure null termination for safety if expecting strings
char buffer[len + 1];
memcpy(buffer, data, len);
buffer[len] = '\0';
ESP_LOGI(RECEIVER_TAG, "Data: %s", buffer);
// You can also access RSSI from recv_info->rx_ctrl->rssi
ESP_LOGI(RECEIVER_TAG, "RSSI: %d", recv_info->rx_ctrl->rssi);
}
static void wifi_init(void) {
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE));
}
void app_main(void) {
ESP_ERROR_CHECK(nvs_flash_init());
wifi_init();
ESP_LOGI(RECEIVER_TAG, "Initializing ESP-NOW...");
ESP_ERROR_CHECK(esp_now_init());
ESP_LOGI(RECEIVER_TAG, "Registering ESP-NOW receive callback...");
ESP_ERROR_CHECK(esp_now_register_recv_cb(app_esp_now_recv_cb));
// Get and print this device's MAC address
uint8_t mac_addr[6];
esp_wifi_get_mac(ESP_IF_WIFI_STA, mac_addr);
ESP_LOGI(RECEIVER_TAG, "My STA MAC: " MACSTR " - Waiting for ESP-NOW messages...", MAC2STR(mac_addr));
}
Build, Flash, and Observe (Examples 1 & 2):
- First, flash a utility to each ESP32 to get its STA MAC address. Note these down.
- Modify
s_peer_mac
inesp_now_sender_main.c
with the MAC address of your receiver ESP32. - Ensure
WIFI_CHANNEL
is the same in both sender and receiver code (e.g., channel 1). - Build and flash the sender code to one ESP32 and the receiver code to another.
- Open serial monitors for both.
- The sender should start sending messages, and its send callback should report success.
- The receiver should print the messages it receives along with the sender’s MAC address and RSSI.
Example 3: Encrypted ESP-NOW Communication
Modify the sender and receiver to use encryption.
Sender (Changes in sender_task
):
// ... (inside sender_task) ...
static const char *s_my_lmk = "THIS_IS_LMK_123"; // 16-byte LMK
esp_now_peer_info_t peer_info = {0};
memcpy(peer_info.peer_addr, s_peer_mac, ESP_NOW_ETH_ALEN);
peer_info.channel = WIFI_CHANNEL;
peer_info.ifidx = ESP_IF_WIFI_STA;
peer_info.encrypt = true; // Enable encryption
memcpy(peer_info.lmk, s_my_lmk, ESP_NOW_KEY_LEN); // Set LMK
if (esp_now_is_peer_exist(s_peer_mac)) {
ESP_LOGI(SENDER_TAG, "Peer " MACSTR " already exists, modifying for encryption.", MAC2STR(s_peer_mac));
ESP_ERROR_CHECK(esp_now_mod_peer(&peer_info)); // Modify if exists
} else {
ESP_LOGI(SENDER_TAG, "Adding encrypted peer " MACSTR, MAC2STR(s_peer_mac));
ESP_ERROR_CHECK(esp_now_add_peer(&peer_info)); // Add if not exists
}
// ... rest of the sending loop ...
Receiver (Changes in app_main before starting tasks, after esp_now_init):
The receiver doesn’t explicitly add the sender as an encrypted peer for receiving encrypted data if the sender initiated the encrypted pairing. However, if the receiver also wants to send encrypted data back, it must add the original sender as an encrypted peer with the same LMK. For just receiving, the key is implicitly handled if the sender has added the receiver as an encrypted peer.
For robust encrypted communication where both can initiate, both should add each other as encrypted peers.
// In receiver's app_main, after esp_now_init() and esp_now_register_recv_cb()
// If the receiver also needs to send encrypted data back, or to ensure it's set up for decryption
// from a specific peer:
static const char *s_my_lmk = "THIS_IS_LMK_123"; // Must be IDENTICAL to sender's LMK
// Replace SENDER_MAC_ADDRESS with the actual MAC of your sender
static uint8_t s_sender_mac_for_encrypt[ESP_NOW_ETH_ALEN] = {0xYY, 0xYY, 0xYY, 0xYY, 0xYY, 0xYY};
esp_now_peer_info_t peer_info_encrypt = {0};
memcpy(peer_info_encrypt.peer_addr, s_sender_mac_for_encrypt, ESP_NOW_ETH_ALEN);
peer_info_encrypt.channel = WIFI_CHANNEL; // Optional, 0 for current
peer_info_encrypt.ifidx = ESP_IF_WIFI_STA;
peer_info_encrypt.encrypt = true;
memcpy(peer_info_encrypt.lmk, s_my_lmk, ESP_NOW_KEY_LEN);
if (esp_now_is_peer_exist(s_sender_mac_for_encrypt)) {
ESP_LOGI(RECEIVER_TAG, "Modifying sender " MACSTR " as encrypted peer.", MAC2STR(s_sender_mac_for_encrypt));
ESP_ERROR_CHECK(esp_now_mod_peer(&peer_info_encrypt));
} else {
ESP_LOGI(RECEIVER_TAG, "Adding sender " MACSTR " as encrypted peer.", MAC2STR(s_sender_mac_for_encrypt));
ESP_ERROR_CHECK(esp_now_add_peer(&peer_info_encrypt));
}
Note: For receiving encrypted data, the crucial part is that the sender added the receiver as an encrypted peer with an LMK. The receiver’s stack uses this LMK from the sender’s “pairing” to decrypt. If the receiver also wants to send encrypted data back, it needs to add the original sender as an encrypted peer with the same LMK.
Test as before. If LMKs don’t match or encryption isn’t set up correctly on the sender for this peer, the receiver won’t get the data, or the sender’s send_cb
might indicate failure.
Variant Notes
ESP-NOW is a feature of the Wi-Fi driver in ESP-IDF and is generally supported across all ESP32 variants that include Wi-Fi capabilities. This includes:
- ESP32 (Original Series): Full ESP-NOW support.
- ESP32-S2: Full ESP-NOW support.
- ESP32-S3: Full ESP-NOW support.
- ESP32-C3: Full ESP-NOW support.
- ESP32-C6: Full ESP-NOW support. It also supports 802.15.4 (Thread/Zigbee), but ESP-NOW remains a Wi-Fi based protocol.
- ESP32-H2: Full ESP-NOW support. Similar to C6, it has 802.15.4 capabilities, but ESP-NOW uses its Wi-Fi radio.
The core ESP-NOW API and functionality are consistent across these variants. Any differences would typically relate to underlying Wi-Fi performance characteristics (range, power consumption) or specific radio coexistence behaviors if other radios (like Bluetooth on ESP32 original, or 802.15.4 on C6/H2) are also active. Always ensure your ESP-IDF version is up-to-date for the best support for your chosen variant.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Wi-Fi Not Initialized or Started | esp_now_init() returns error (e.g., ESP_ERR_ESPNOW_IF, ESP_ERR_ESPNOW_WIFI_NOT_INIT). ESP-NOW functions fail or behave unpredictably. |
Ensure proper Wi-Fi initialization sequence:
1. nvs_flash_init() 2. esp_netif_init() 3. esp_event_loop_create_default() 4. wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&cfg); 5. esp_wifi_set_mode(WIFI_MODE_STA or WIFI_MODE_AP); 6. esp_wifi_start(); Only then call esp_now_init(). |
Mismatched Wi-Fi Channels | No communication between devices. esp_now_send_cb reports ESP_NOW_SEND_FAIL. Receiver gets no data. |
Set the same Wi-Fi channel on all ESP-NOW devices:
ESP_ERROR_CHECK(esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE)); If using peer-specific channels in esp_now_add_peer(), ensure these are correctly configured and match the peer’s actual channel. |
Incorrect Peer MAC Address | Unicast messages fail (ESP_NOW_SEND_FAIL). Data not received by the intended peer. |
Double-check MAC addresses. Print MACs on boot for verification:
uint8_t mac[6]; esp_wifi_get_mac(ESP_IF_WIFI_STA, mac); ESP_LOGI(TAG, “MAC: %02X:%02X:%02X:%02X:%02X:%02X”, mac[0]..mac[5]); |
Encryption Key (LMK) Mismatch / Setup | Encrypted communication fails. Sender gets ESP_NOW_SEND_FAIL. Receiver doesn’t get data or gets corrupted data (if it somehow passes MAC layer). |
Ensure the 16-byte LMK is identical on both sender and receiver for that specific peer relationship.
Both peers must have encrypt = true; in their esp_now_peer_info_t for each other. Check CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM if adding many encrypted peers. |
Blocking Operations in Callbacks | Delayed processing of subsequent ESP-NOW events, watchdog timeouts, system instability, missed packets. |
Keep esp_now_send_cb and esp_now_recv_cb very short and non-blocking.
Offload data processing to separate FreeRTOS tasks using queues or event groups. |
Exceeding Max Payload (250 bytes) | esp_now_send() returns ESP_ERR_ESPNOW_ARG or ESP_ERR_INVALID_ARG, or transmission fails silently. | Limit data payload to 250 bytes. For larger data, implement fragmentation/reassembly at the application layer. |
Peer List Full | esp_now_add_peer() returns ESP_ERR_ESPNOW_PEER_LIST_FULL. | Check peer limits. Default is ~20 unencrypted peers, fewer (CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM, often 6-7) if encryption is used. Manage peer list actively if dealing with many dynamic peers. |
Send/Receive Callbacks Not Registered | No confirmation of sent packets. No received data processed even if packets arrive. |
Ensure callbacks are registered after esp_now_init():
ESP_ERROR_CHECK(esp_now_register_send_cb(app_esp_now_send_cb)); ESP_ERROR_CHECK(esp_now_register_recv_cb(app_esp_now_recv_cb)); |
Exercises
- Remote LED Control with Bidirectional ACK:
- Device A (Controller): Has a button. When pressed, sends an “LED_TOGGLE” command to Device B via ESP-NOW. It should also have an LED to indicate if the command was successfully acknowledged by Device B.
- Device B (Actuator): Has an LED. When it receives “LED_TOGGLE”, it toggles its LED and sends back an “ACK_LED_TOGGLED” message to Device A.
- Device A lights up its ACK LED upon receiving “ACK_LED_TOGGLED”.
- Multi-Sensor Network to Gateway:
- Sensor Nodes (2-3 ESP32s): Each simulates reading a different sensor value (e.g., temperature, humidity – can be random numbers for simulation). Periodically, each sensor node sends its MAC address and sensor value to a designated Gateway ESP32 using unicast ESP-NOW.
- Gateway Node (1 ESP32): Receives data from all sensor nodes. It should print the MAC address of the sending sensor and its data. Implement a simple way to manage adding sensor nodes as peers (e.g., hardcode MACs or a very simple registration broadcast).
- Encrypted Control System:
- Set up two ESP32s for encrypted ESP-NOW communication.
- Device X (Master): Sends encrypted commands like “MOTOR_START”, “MOTOR_STOP”, “SET_SPEED_XX” (where XX is a value).
- Device Y (Slave): Receives and decrypts these commands. It should log the received command. (No actual motor control needed, just log).
- Ensure both devices use the same LMK.
- ESP-NOW Broadcast Beacon and Unicast Response:
- Device R (Receiver/Responder): Initializes ESP-NOW and listens for broadcast messages.
- Device S (Sender/Scanner): Periodically sends a broadcast “DISCOVERY_PROBE” message.
- When Device R receives “DISCOVERY_PROBE”, it extracts the sender’s MAC address from the
recv_info
and adds Device S as a peer. Then, it sends a unicast “DISCOVERY_RESPONSE” message back to Device S. - Device S, upon receiving “DISCOVERY_RESPONSE”, logs the MAC address of the responder. This simulates a basic device discovery mechanism.
Summary
- ESP-NOW is an Espressif-proprietary, connectionless protocol for fast, low-latency, direct device-to-device communication using Wi-Fi hardware.
- It requires Wi-Fi to be initialized (typically in STA mode) and devices to be on the same Wi-Fi channel.
- Communication is MAC address-based, supporting unicast and a form of broadcast to registered peers.
- Key API functions include
esp_now_init()
,esp_now_add_peer()
,esp_now_send()
,esp_now_register_send_cb()
, andesp_now_register_recv_cb()
. - ESP-NOW supports optional AES-CCMP encryption by setting a shared Local Master Key (LMK) for peer entries.
- The maximum data payload per message is 250 bytes.
- It is supported on all ESP32 variants with Wi-Fi capabilities (ESP32, S2, S3, C3, C6, H2).
Further Reading
- ESP-IDF Programming Guide – ESP-NOW: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/network/esp_now.html (Replace
esp32
with your specific chip if needed, though the API is largely consistent). - ESP-IDF API Reference –
esp_now.h
: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/network/esp_now.html - Espressif ESP-NOW Examples on GitHub: https://github.com/espressif/esp-now