ESP-NOW Protocol for Direct Device Communication

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;
  1. Initializing NVS (Non-Volatile Storage), as Wi-Fi calibration data might be stored there.
  2. Initializing the TCP/IP adapter stack (esp_netif_init()).
  3. Creating a default event loop (esp_event_loop_create_default()).
  4. Initializing Wi-Fi with default configuration (wifi_init_config_t).
  5. 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.
  6. Starting Wi-Fi (esp_wifi_start()).
  7. Initializing ESP-NOW using esp_now_init().
  8. 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 if ifidx allows.
    • ifidx: Wi-Fi interface (WIFI_IF_STA or WIFI_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. Use NULL 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 if peer_addr is NULL, 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). The esp_now_recv_info_t structure now contains src_addr (sender’s MAC) and rx_ctrl (RSSI and other PHY info).
    • 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 the channel field in esp_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. If channel 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 in esp_now_peer_info_t.
  • Provide a 16-byte Local Master Key (LMK) in the lmk field of esp_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:

C
#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):

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):

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):

  1. First, flash a utility to each ESP32 to get its STA MAC address. Note these down.
  2. Modify s_peer_mac in esp_now_sender_main.c with the MAC address of your receiver ESP32.
  3. Ensure WIFI_CHANNEL is the same in both sender and receiver code (e.g., channel 1).
  4. Build and flash the sender code to one ESP32 and the receiver code to another.
  5. Open serial monitors for both.
  6. The sender should start sending messages, and its send callback should report success.
  7. 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):

C
// ... (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.

C
// 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

  1. 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”.
  2. 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).
  3. 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.
  4. 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(), and esp_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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top