Chapter 74: ESP-NOW Multi-device Networks

Chapter Objectives

By the end of this chapter, you will be able to:

  • Design and implement basic ESP-NOW network topologies such as star and simple relay configurations.
  • Effectively manage ESP-NOW peer lists in applications involving multiple devices.
  • Implement application-level data forwarding to achieve multi-hop communication.
  • Utilize unicast and broadcast ESP-NOW messages effectively within a multi-device network.
  • Understand strategies for basic device discovery and dynamic peer management in an ESP-NOW network.
  • Recognize the scalability considerations and inherent limitations when building larger networks with ESP-NOW.
  • Develop ESP32 applications that form simple, coordinated networks using ESP-NOW.

Introduction

Previous chapters introduced ESP-NOW as a fast and efficient protocol for direct peer-to-peer communication between ESP32 devices. While its core strength lies in simple, connectionless links, ESP-NOW can also serve as the foundation for creating small, localized networks of multiple devices. This capability is particularly useful for scenarios where several devices need to coordinate actions, exchange sensor data, or respond to commands within a limited area, all without the overhead of a traditional Wi-Fi access point for their intercommunication.

Imagine a scenario with multiple environmental sensors spread across a room, all reporting their data to a central ESP32 data logger. Or consider a set of custom wireless remote controls interacting with various actuators in a home automation project. These are prime examples where a multi-device ESP-NOW network can provide a responsive and power-efficient solution.

This chapter will guide you through the principles and practices of building such multi-device networks using ESP-NOW. We will explore common network structures, techniques for managing multiple peers, implementing simple data relaying, and the considerations for scalability and reliability. It’s important to remember that ESP-NOW is not a self-routing mesh protocol; any multi-hop capabilities must be explicitly built at the application layer.

Theory

ESP-NOW Network Topologies

While ESP-NOW is fundamentally a peer-to-peer protocol, you can arrange devices in various logical network structures at the application level:

  1. Star Network:
    • Structure: A central node (often called a master, hub, or gateway) communicates directly with multiple peripheral nodes (slaves or end-devices). Peripheral nodes typically only communicate with the central node, not directly with each other.
    • Communication Flow:
      • Many-to-one: Slaves send data (e.g., sensor readings) to the master.
      • One-to-many: Master sends commands or configuration data to slaves (can use unicast to specific slaves or broadcast to all its registered slave peers).
    • Pros: Simple to manage, centralized data collection and control.
    • Cons: The central node is a single point of failure. Range is limited by the direct reach of the central node to each peripheral.
  2. Line or Tree Network (with Application-Layer Relaying):
    • Structure: Devices are arranged in a line or a hierarchical tree. Messages can be relayed from one node to the next to extend range or cover more complex areas.
    • Communication Flow: A node receives a message. If it’s not the final destination (based on application-level addressing in the payload), it retransmits the message to the next appropriate peer in the chain/tree.
    • Pros: Can extend range beyond a single hop.
    • Cons: Latency increases with each hop. Relaying logic must be implemented in the application. More complex to manage than a star network. Susceptible to failure if a relay node goes down.
  3. Mesh-like Ad-hoc Networks (Simple Relaying):
    • Structure: Devices can potentially communicate with multiple other devices within their radio range. This is not a true self-healing, self-routing mesh network like Zigbee or Thread.
    • Communication Flow: Similar to tree networks, relaying is application-defined. A device might forward a message to one or more neighbors based on some criteria.
    • Pros: Offers flexibility.
    • Cons: Can become very complex to manage without a proper mesh routing protocol. Prone to issues like message flooding, routing loops, and inefficient paths if not carefully designed. ESP-NOW itself does not provide mesh routing.

Peer Management in Multi-Device Networks

Effectively managing the peer list on each ESP32 is crucial in a multi-device network.

  • Master/Gateway Node (Star Network): The central node must add the MAC addresses of all slave devices it needs to communicate with or receive data from. It will have the largest peer list.
  • Slave Nodes (Star Network): Each slave node typically only needs to add the MAC address of the master node as its peer.
  • Relay Nodes: A relay node must add peers for both the “upstream” device (from which it receives) and the “downstream” device(s) (to which it forwards).
  • Peer Limits:
    • CONFIG_ESP_WIFI_ESPNOW_MAX_PEER_NUM: Defines the total number of peers (encrypted and unencrypted) a device can manage (default usually 20).
    • CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM: Defines the maximum number of these peers that can use encryption (default usually 6-7).
    • These limits can be adjusted in menuconfig but are ultimately constrained by available RAM. For a central node in a large star network, this limit can be a bottleneck.
Kconfig Option Description Default Value (Typical) Impact & Considerations
CONFIG_ESP_WIFI_ESPNOW_MAX_PEER_NUM Defines the total maximum number of ESP-NOW peers (encrypted and unencrypted) that a single ESP32 device can manage simultaneously. 20 This is a hard limit per device. A central node in a large star network might reach this limit. Increasing it consumes more RAM. Affects how many devices can be directly registered.
CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM Defines the maximum number of ESP-NOW peers (out of the total MAX_PEER_NUM) that can use encryption (LMK – Local Master Key). 6 or 7 (varies slightly by ESP-IDF version/chip) Encrypted communication is more secure but resource-intensive. This limits the number of secure links. If more than this number of peers are added with encrypt = true, esp_now_add_peer() may fail for the additional encrypted peers.
RAM Usage Each peer entry consumes a certain amount of RAM for storing peer information (MAC address, channel, LMK if encrypted, etc.). Be mindful of overall RAM consumption, especially on ESP32 variants with less available memory, when increasing these limits.
Configuration These values are configured via menuconfig under “Component config” -> “ESP Wi-Fi”. Changes require recompiling the project. Always test thoroughly after modifying these defaults.

Communication Patterns

  1. Unicast: Sending a message to a single, specific peer MAC address. This is the standard mode for targeted communication and is required for encrypted messages.
  2. Broadcast:
    • Sending a message using esp_now_send(NULL, ...) or esp_now_send(broadcast_mac, ...) where broadcast_mac is FF:FF:FF:FF:FF:FF.
    • Sender Behavior: If peer_addr is NULL or the broadcast MAC, the sender iterates through its entire list of registered peers and sends a copy of the message to each one that matches the interface and channel criteria (or to all if channel is 0 for peers). It does not send a generic broadcast packet that any listening ESP-NOW device can pick up without prior registration.
    • Receiver Behavior: For a device to receive a message sent by a sender using the broadcast MAC, that receiver must typically have the sender added as a peer (often with the sender’s specific MAC, or sometimes with the broadcast MAC if the receiver is intended to be a generic listener for broadcasts from any pre-configured “broadcaster” peer).
    • No Encryption: Broadcast messages are not encrypted by ESP-NOW.
    • No ACKs for Broadcast: When sending to the broadcast MAC address, the send callback (esp_now_send_cb_t) will be invoked once for the broadcast attempt itself, typically with a success status if the transmission was initiated, but it doesn’t reflect individual ACKs from recipients. It’s a “fire-and-forget” mechanism at the ESP-NOW layer for broadcast.
Feature Unicast ESP-NOW Broadcast ESP-NOW
Target Recipient(s) Single, specific peer MAC address. All registered peers (matching interface/channel) if peer MAC is NULL or FF:FF:FF:FF:FF:FF. Not a general Wi-Fi broadcast to non-peers.
esp_now_send() Call esp_now_send(peer_mac, data, len); esp_now_send(NULL, data, len); or
esp_now_send(broadcast_mac, data, len);
Encryption Supported (if peer is added with encryption enabled and LMK set). Not supported. Messages are sent unencrypted.
Acknowledgment (ACK) Yes, standard Wi-Fi ACK mechanism. Send callback (esp_now_send_cb_t) indicates success/failure of transmission to the specific peer. No individual ACKs. Send callback is invoked once for the broadcast attempt, usually with success if initiated, but doesn’t confirm receipt by any specific peer. “Fire-and-forget.”
Peer Registration Receiver must have added the sender as a peer to typically process the message (and sender must add receiver to send). Receiver must generally have the sender added as a peer (with sender’s MAC or sometimes broadcast MAC if configured as a generic listener) to receive the broadcast.
Use Cases
  • Targeted data to a specific device.
  • Commands to a single slave.
  • Secure communication.
  • Reliable, acknowledged messages.
  • General announcements to all known devices (e.g., “System Alert”).
  • Discovery probes.
  • Synchronization signals.
  • Commands to a group of configured peers.
Efficiency Efficient for one-to-one. Can be efficient for one-to-many if all registered peers need the same data. Avoids multiple unicast calls.

Application-Layer Data Forwarding/Relaying

Since ESP-NOW is a single-hop protocol, extending communication beyond direct radio range requires relaying messages at the application layer.

Basic Relay Logic:

  1. Node A wants to send data to Node C, but C is out of A’s direct range. Node B is in range of both A and C.
  2. Node A adds Node B as a peer and sends the message to B. The payload of this message must include information indicating the ultimate destination (Node C’s identifier/MAC) and potentially the original source (Node A’s identifier/MAC).
  3. Node B receives the message from A. Its application logic inspects the payload.
  4. Node B sees that the message is for Node C. Node B adds Node C as a peer (if not already added).
  5. Node B retransmits the original payload (or a modified one if needed) to Node C.
  6. Node C receives the message.
%%{init: {"fontFamily": "Open Sans"}}%%
sequenceDiagram
    actor NodeA as Node A (Source)
    participant NodeB as Node B (Relay)
    actor NodeC as Node C (Final Destination)

    NodeA->>NodeB: esp_now_send(B_MAC, payload_for_C)
    activate NodeB
    Note over NodeB: Receives ESP-NOW frame from A
    NodeB->>NodeB: App Logic: Inspect payload<br>(final_dest_mac == C_MAC?)
    alt Message is for C
        NodeB->>NodeC: esp_now_send(C_MAC, original_payload_for_C)
        activate NodeC
        Note over NodeC: Receives ESP-NOW frame from B
        NodeC->>NodeC: App Logic: Process payload<br>(orig_src_mac == A_MAC)
        deactivate NodeC
    else Message not for C (or other logic)
        NodeB->>NodeB: Handle differently (e.g., drop, error)
    end
    deactivate NodeB



Challenges in Relaying:

Challenge Description Potential Solutions / Considerations
Addressing How to specify the final destination and original source within the limited ESP-NOW payload (max 250 bytes).
  • Define a custom packet structure within the payload.
  • Include fields for final_dest_mac, original_src_mac.
  • Use compact identifiers if MACs are too large for complex routes.
Routing Logic How does a relay node determine the next hop for a given final destination, especially if multiple paths exist?
  • Static Routes: Pre-configure routes on relay nodes. Simple but inflexible.
  • Simple Rules: E.g., “if for network segment X, forward to relay Y.”
  • ESP-NOW does not provide dynamic routing protocols like AODV or RPL.
Loop Prevention Messages could be relayed in circles in networks with redundant paths, consuming bandwidth and resources.
  • Time-To-Live (TTL): Add a TTL counter to the payload, decrement at each hop. Drop if TTL reaches zero.
  • Careful network design to avoid loops.
  • Sequence numbers to detect duplicate messages.
Latency Each relay hop introduces processing and transmission delay. Overall latency increases with the number of hops.
  • Minimize the number of hops where possible.
  • Optimize relay node processing code.
  • Unavoidable to some extent; factor into application requirements.
Reliability Each hop is a point of potential failure (node down, RF interference). ESP-NOW ACKs are only hop-to-hop.
  • Application-Level ACKs: Implement end-to-end acknowledgments if high reliability is needed.
  • Retry mechanisms at each hop or end-to-end.
  • Redundant paths (complex to manage without true mesh).
Increased Complexity Application code on relay nodes becomes more complex than simple end-nodes.
  • Modular design for relaying logic.
  • Thorough testing of relay functions.
Power Consumption Relay nodes are always on (or wake frequently) to listen and forward, increasing their power consumption.
  • Optimize wake/sleep cycles if possible, but relay duty is demanding.
  • Consider mains power for critical relay nodes.

Device Discovery and Dynamic Peer Addition

In some scenarios, especially with more ad-hoc networks, devices might not have prior knowledge of all other peers’ MAC addresses.

Simple Discovery Mechanism:

  1. Probing Node: A new node (or any node wanting to find others) can send a broadcast ESP-NOW message (e.g., “DISCOVERY_PROBE”) to all its currently configured broadcast-receptive peers or to the generic broadcast MAC if it expects responses from any listening device configured to respond. (Remember ESP-NOW broadcast behavior).
    • A more effective way for general discovery is for the new node to operate on a known channel and listen. Other devices that want to be discoverable can periodically send an ESP-NOW broadcast message (e.g., “HERE_I_AM” containing their MAC) on that channel.
  2. Responding Node(s): Devices receiving the probe (and configured to respond) can send a unicast ESP-NOW message back to the probing node’s MAC address (which is available in the recv_info->src_addr of the receive callback). This response can include their own MAC address or other identifying information.
  3. Peer Addition: The probing node, upon receiving responses, can then add the responders as peers using esp_now_add_peer().
%%{init: {"fontFamily": "Open Sans"}}%%
sequenceDiagram
    actor NewNode as New Node (Prober)
    participant Network as ESP-NOW Network (Known Channel)
    actor ExistingNode1 as Discoverable Node 1
    actor ExistingNode2 as Discoverable Node 2

    NewNode->>Network: Sends ESP-NOW Broadcast ("DISCOVERY_PROBE")
    activate ExistingNode1
    activate ExistingNode2
    Note over ExistingNode1,ExistingNode2: Both receive Probe
    
    ExistingNode1-->>NewNode: Unicast ESP-NOW Response<br>(incl. own MAC/Info)
    deactivate ExistingNode1
    
    ExistingNode2-->>NewNode: Unicast ESP-NOW Response<br>(incl. own MAC/Info)
    deactivate ExistingNode2

    activate NewNode
    Note over NewNode: Receives responses
    NewNode->>NewNode: App Logic: esp_now_add_peer(ExistingNode1_MAC)
    NewNode->>NewNode: App Logic: esp_now_add_peer(ExistingNode2_MAC)
    deactivate NewNode


This is a very basic form of discovery. More robust mechanisms might involve specific discovery channels or time windows.

Practical Examples

Prerequisites:

  • At least three ESP32 boards for some examples.
  • VS Code with the Espressif IDF Extension.
  • ESP-IDF v5.x.
  • MAC addresses of your ESP32 boards.

Example 1: Star Network – Sensor Data Aggregation

  • Master Node (1 ESP32): Receives data from two Slave Nodes.
  • Slave Nodes (2 ESP32s): Each sends a unique message to the Master.

Master Node Code (main/master_node_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 MASTER_TAG "ESP_NOW_MASTER"
#define WIFI_CHANNEL 1

// MAC Addresses of the Slave Nodes (Replace with actual MACs)
static uint8_t s_slave1_mac[ESP_NOW_ETH_ALEN] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x01};
static uint8_t s_slave2_mac[ESP_NOW_ETH_ALEN] = {0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0x02};

// ESP-NOW Receive Callback
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(MASTER_TAG, "Receive CB error");
        return;
    }
    const uint8_t *mac_addr = recv_info->src_addr;
    ESP_LOGI(MASTER_TAG, "Received %d bytes from " MACSTR ":", len, MAC2STR(mac_addr));
    char buffer[len + 1];
    memcpy(buffer, data, len);
    buffer[len] = '\0';
    ESP_LOGI(MASTER_TAG, "Data: %s (RSSI: %d)", buffer, recv_info->rx_ctrl->rssi);

    // Example: Send an ACK back to the slave (optional)
    // char ack_msg[] = "Master ACK";
    // esp_now_send(mac_addr, (uint8_t*)ack_msg, strlen(ack_msg));
}

// ESP-NOW Send Callback (useful if master sends commands or ACKs)
static void app_esp_now_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) {
    ESP_LOGI(MASTER_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)); // STA mode is fine
    ESP_ERROR_CHECK(esp_wifi_start());
    ESP_ERROR_CHECK(esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE));
    // Optional: Set a fixed MAC for easier debugging if needed, usually not necessary
    // uint8_t master_mac[6] = {0xCC,0xCC,0xCC,0xCC,0xCC,0x00};
    // ESP_ERROR_CHECK(esp_wifi_set_mac(WIFI_IF_STA, master_mac));
}

void app_main(void) {
    ESP_ERROR_CHECK(nvs_flash_init());
    wifi_init();

    ESP_LOGI(MASTER_TAG, "Initializing ESP-NOW Master...");
    ESP_ERROR_CHECK(esp_now_init());
    ESP_ERROR_CHECK(esp_now_register_recv_cb(app_esp_now_recv_cb));
    ESP_ERROR_CHECK(esp_now_register_send_cb(app_esp_now_send_cb)); // For sending ACKs/commands

    // Add Slave 1 as peer
    esp_now_peer_info_t peer_info_slave1 = {0};
    memcpy(peer_info_slave1.peer_addr, s_slave1_mac, ESP_NOW_ETH_ALEN);
    peer_info_slave1.channel = WIFI_CHANNEL;
    peer_info_slave1.ifidx = ESP_IF_WIFI_STA;
    peer_info_slave1.encrypt = false; // Set to true and add LMK if encryption needed
    ESP_ERROR_CHECK(esp_now_add_peer(&peer_info_slave1));
    ESP_LOGI(MASTER_TAG, "Added Slave 1 (" MACSTR ") as peer.", MAC2STR(s_slave1_mac));

    // Add Slave 2 as peer
    esp_now_peer_info_t peer_info_slave2 = {0};
    memcpy(peer_info_slave2.peer_addr, s_slave2_mac, ESP_NOW_ETH_ALEN);
    peer_info_slave2.channel = WIFI_CHANNEL;
    peer_info_slave2.ifidx = ESP_IF_WIFI_STA;
    peer_info_slave2.encrypt = false;
    ESP_ERROR_CHECK(esp_now_add_peer(&peer_info_slave2));
    ESP_LOGI(MASTER_TAG, "Added Slave 2 (" MACSTR ") as peer.", MAC2STR(s_slave2_mac));
    
    uint8_t my_mac[6];
    esp_wifi_get_mac(ESP_IF_WIFI_STA, my_mac);
    ESP_LOGI(MASTER_TAG, "Master MAC: " MACSTR " - Waiting for data on channel %d...", MAC2STR(my_mac), WIFI_CHANNEL);
}

Slave Node Code (e.g., main/slave1_main.c – similar for slave2 with different message/MAC):

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 SLAVE_TAG "ESP_NOW_SLAVE1" // Change to SLAVE2 for the other slave
#define WIFI_CHANNEL 1

// MAC Address of the Master Node (Replace with actual MAC)
static uint8_t s_master_mac[ESP_NOW_ETH_ALEN] = {0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00}; 

static void app_esp_now_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) {
    ESP_LOGI(SLAVE_TAG, "Send CB to Master " MACSTR ": %s", MAC2STR(mac_addr),
             (status == ESP_NOW_SEND_SUCCESS) ? "Success" : "Fail");
}

// Optional: Receive callback if master sends ACKs or commands
// static void app_esp_now_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { ... }

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));
    // Optional: Set a fixed MAC for easier debugging if needed
    // uint8_t slave1_mac[6] = {0xAA,0xAA,0xAA,0xAA,0xAA,0x01};
    // ESP_ERROR_CHECK(esp_wifi_set_mac(WIFI_IF_STA, slave1_mac));
}

void sender_task(void *pvParameter) {
    esp_now_peer_info_t peer_info_master = {0};
    memcpy(peer_info_master.peer_addr, s_master_mac, ESP_NOW_ETH_ALEN);
    peer_info_master.channel = WIFI_CHANNEL;
    peer_info_master.ifidx = ESP_IF_WIFI_STA;
    peer_info_master.encrypt = false;
    ESP_ERROR_CHECK(esp_now_add_peer(&peer_info_master));
    ESP_LOGI(SLAVE_TAG, "Added Master (" MACSTR ") as peer.", MAC2STR(s_master_mac));

    uint32_t count = 0;
    char send_data[100];

    while (true) {
        // For Slave 2, change the message slightly, e.g., "Data from SLAVE 2"
        sprintf(send_data, "Data from SLAVE 1! Count: %" PRIu32, count++); 
        esp_err_t ret = esp_now_send(s_master_mac, (uint8_t *)send_data, strlen(send_data));
        if (ret == ESP_OK) {
            ESP_LOGI(SLAVE_TAG, "Sent to Master: %s", send_data);
        } else {
            ESP_LOGE(SLAVE_TAG, "Send to Master error: %s", esp_err_to_name(ret));
        }
        vTaskDelay(pdMS_TO_TICKS(5000 + (esp_random() % 2000))); // Send at slightly different intervals
    }
}

void app_main(void) {
    ESP_ERROR_CHECK(nvs_flash_init());
    wifi_init();

    ESP_LOGI(SLAVE_TAG, "Initializing ESP-NOW Slave...");
    ESP_ERROR_CHECK(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)); // If expecting ACKs/commands

    uint8_t my_mac[6];
    esp_wifi_get_mac(ESP_IF_WIFI_STA, my_mac);
    ESP_LOGI(SLAVE_TAG, "My STA MAC: " MACSTR, MAC2STR(my_mac));

    xTaskCreate(sender_task, "slave_sender_task", 4096, NULL, 5, NULL);
}

Build, Flash, Observe:

  1. Get MAC addresses of all three ESP32s.
  2. Update s_slave1_mac, s_slave2_mac in master_node_main.c.
  3. Update s_master_mac in slave1_main.c and slave2_main.c.
  4. Ensure WIFI_CHANNEL is the same for all.
  5. Build and flash each project to its respective ESP32.
  6. Open serial monitors for all three.
  7. The slave nodes should start sending data, and the master node should log the received data along with the source MAC address.

Example 2: Simple Application-Layer Relay Node

  • Node A (Source): Sends “Hello Relay World!” destined for Node C.
  • Node B (Relay): Receives from A, sees it’s for C, forwards to C.
  • Node C (Destination): Receives message from B (originated by A).

Message Structure (Application Defined):

To enable relaying, we need to define a simple message structure within the ESP-NOW payload.

C
typedef struct {
    uint8_t final_dest_mac[ESP_NOW_ETH_ALEN];
    uint8_t original_src_mac[ESP_NOW_ETH_ALEN];
    uint8_t actual_payload[ESP_NOW_MAX_DATA_LEN - 2 * ESP_NOW_ETH_ALEN - 2]; // Reserve space for type/len
    // Add more fields like message_type, payload_len, ttl if needed
} relayed_message_t;

This is a basic example. Real relaying would need more robust packet formats.

Node A (Source) – Snippet (main/node_a_main.c):

C
// ... (wifi_init, send_cb, esp_now_init as before) ...
// MACs of B and C
static uint8_t s_node_b_mac[ESP_NOW_ETH_ALEN] = {/* B's MAC */};
static uint8_t s_node_c_mac[ESP_NOW_ETH_ALEN] = {/* C's MAC */};
uint8_t my_mac_a[ESP_NOW_ETH_ALEN];

void node_a_task(void *param) {
    esp_wifi_get_mac(ESP_IF_WIFI_STA, my_mac_a);
    // Add Node B as peer
    esp_now_peer_info_t peer_b = {0};
    memcpy(peer_b.peer_addr, s_node_b_mac, ESP_NOW_ETH_ALEN);
    peer_b.channel = WIFI_CHANNEL; peer_b.ifidx = ESP_IF_WIFI_STA; peer_b.encrypt = false;
    ESP_ERROR_CHECK(esp_now_add_peer(&peer_b));

    relayed_message_t msg_to_send;
    memcpy(msg_to_send.final_dest_mac, s_node_c_mac, ESP_NOW_ETH_ALEN);
    memcpy(msg_to_send.original_src_mac, my_mac_a, ESP_NOW_ETH_ALEN);
    strcpy((char*)msg_to_send.actual_payload, "Hello Relay World from A!");

    while(1) {
        esp_err_t result = esp_now_send(s_node_b_mac, (uint8_t*)&msg_to_send, sizeof(relayed_message_t));
        ESP_LOGI("NODE_A", "Sent to B for C, status: %s", esp_err_to_name(result));
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}
// ... (app_main calls node_a_task) ...

Node B (Relay) – Snippet (main/node_b_main.c):

C
// ... (wifi_init, send_cb, esp_now_init as before) ...
// MACs of A and C
static uint8_t s_node_a_mac[ESP_NOW_ETH_ALEN] = {/* A's MAC */};
static uint8_t s_node_c_mac[ESP_NOW_ETH_ALEN] = {/* C's MAC */};
uint8_t my_mac_b[ESP_NOW_ETH_ALEN];

void app_esp_now_recv_cb_node_b(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) {
    esp_wifi_get_mac(ESP_IF_WIFI_STA, my_mac_b);
    if (len >= sizeof(relayed_message_t)) {
        relayed_message_t *received_msg = (relayed_message_t *)data;
        ESP_LOGI("NODE_B", "Received msg from " MACSTR " for " MACSTR, MAC2STR(recv_info->src_addr), MAC2STR(received_msg->final_dest_mac));
        ESP_LOGI("NODE_B", "Original sender: " MACSTR ", Payload: %s", MAC2STR(received_msg->original_src_mac), received_msg->actual_payload);

        // Check if this node is the final destination (should not be for relay)
        if (memcmp(received_msg->final_dest_mac, my_mac_b, ESP_NOW_ETH_ALEN) == 0) {
            ESP_LOGI("NODE_B", "Message is for me. Processing.");
            // Process locally if B was also a potential destination
        } else {
            // This is a simple relay, assuming we know C is the next hop
            // In a real system, you'd have routing logic here
            ESP_LOGI("NODE_B", "Relaying message to Node C (" MACSTR ")", MAC2STR(s_node_c_mac));
            esp_err_t result = esp_now_send(s_node_c_mac, data, len); // Forward the exact same data
            ESP_LOGI("NODE_B", "Relay send status to C: %s", esp_err_to_name(result));
        }
    }
}
// In app_main for Node B:
// esp_wifi_get_mac(ESP_IF_WIFI_STA, my_mac_b);
// Add Node A as peer (to receive from A - not strictly needed if A just sends, but good practice)
// esp_now_peer_info_t peer_a = {0}; ... esp_now_add_peer(&peer_a);
// Add Node C as peer (to send to C)
// esp_now_peer_info_t peer_c = {0}; memcpy(peer_c.peer_addr, s_node_c_mac, ...); esp_now_add_peer(&peer_c);
// esp_now_register_recv_cb(app_esp_now_recv_cb_node_b);

Node C (Destination) – Snippet (main/node_c_main.c):

C
// ... (wifi_init, esp_now_init as before) ...
// MAC of B (its direct sender)
static uint8_t s_node_b_mac[ESP_NOW_ETH_ALEN] = {/* B's MAC */};
uint8_t my_mac_c[ESP_NOW_ETH_ALEN];

void app_esp_now_recv_cb_node_c(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) {
    esp_wifi_get_mac(ESP_IF_WIFI_STA, my_mac_c);
    if (len >= sizeof(relayed_message_t)) {
        relayed_message_t *received_msg = (relayed_message_t *)data;
        ESP_LOGI("NODE_C", "Received msg from direct sender " MACSTR, MAC2STR(recv_info->src_addr)); // This will be B's MAC
        // Check if this node is the final destination
        if (memcmp(received_msg->final_dest_mac, my_mac_c, ESP_NOW_ETH_ALEN) == 0) {
            ESP_LOGI("NODE_C", "Message is for me!");
            ESP_LOGI("NODE_C", "Original sender: " MACSTR ", Payload: %s", MAC2STR(received_msg->original_src_mac), received_msg->actual_payload);
        } else {
            ESP_LOGW("NODE_C", "Received relayed message not for me? Final Dest: " MACSTR, MAC2STR(received_msg->final_dest_mac));
        }
    }
}
// In app_main for Node C:
// esp_wifi_get_mac(ESP_IF_WIFI_STA, my_mac_c);
// Add Node B as peer (to receive from B - not strictly needed for just receiving if B initiates, but good for context)
// esp_now_peer_info_t peer_b = {0}; memcpy(peer_b.peer_addr, s_node_b_mac, ...); esp_now_add_peer(&peer_b);
// esp_now_register_recv_cb(app_esp_now_recv_cb_node_c);

Build, Flash, Observe:

  1. Update all MAC addresses in all three projects.
  2. Flash each project to a separate ESP32.
  3. Ensure all are on the same WIFI_CHANNEL.
  4. Node A sends to B. B should log reception and then log that it’s relaying to C. Node C should log reception, showing the original sender was A.

Note: This relay example is highly simplified. Real-world relaying needs robust error handling, potentially routing tables if multiple paths exist, loop prevention (e.g., TTL), and end-to-end acknowledgments if reliability is paramount.

Variant Notes

The core ESP-NOW APIs for peer management, sending, and receiving data are consistent across all ESP32 variants that support Wi-Fi:

  • ESP32 (Original Series), ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2: All support multi-device ESP-NOW networks as described.

The primary considerations that might vary slightly between variants would be:

  • Default/Maximum Peer Limits: While CONFIG_ESP_WIFI_ESPNOW_MAX_PEER_NUM and CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM are software configurations, the absolute maximums might be influenced by available RAM, which can differ. Always check the Kconfig descriptions for these.
  • RF Performance: Subtle differences in radio sensitivity or output power characteristics between chip generations could lead to minor variations in achievable range or reliability in dense networks, but the protocol operation remains the same.
  • Coexistence with Other Radios:
    • Original ESP32: Coexistence with Bluetooth Classic needs to be managed if BTDM mode is used.
    • ESP32-C6, ESP32-H2: These also have 802.15.4 radios. If Thread/Zigbee are active concurrently with Wi-Fi/ESP-NOW, radio time-sharing and potential interference need to be considered, though Espressif’s drivers aim to manage this. For pure ESP-NOW networks, this is less of a concern.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect MAC Addresses in Peer Setup Messages don’t reach intended unicast peer; send callbacks report failure (ESP_NOW_SEND_FAIL). Master doesn’t see specific slaves. Meticulously verify all MAC addresses used in esp_now_add_peer(). Print MACs on device boot (e.g., esp_wifi_get_mac()) for easy cross-referencing. Ensure correct slave MACs on master and correct master MAC on slaves.
Channel Mismatches Across the Network Complete communication failure between nodes or groups of nodes. No messages received. Ensure ALL ESP32s in the ESP-NOW network are configured to use the exact same Wi-Fi channel via esp_wifi_set_channel().
Exceeding Peer Limits (CONFIG_ESP_WIFI_ESPNOW_MAX_PEER_NUM) esp_now_add_peer() returns ESP_ERR_ESPNOW_PEER_LIST_FULL or similar error. Central/relay node cannot add more devices. Check peer count. Increase limit in menuconfig if RAM allows, or redesign network (e.g., use relay hierarchies, reduce direct peers for a single node).
Exceeding Encrypted Peer Limits (CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM) esp_now_add_peer() fails when adding an encrypted peer, even if total peer count is below MAX_PEER_NUM. Ensure the number of peers added with encrypt = true does not exceed this limit. Use encryption selectively.
Flawed Application-Layer Relaying Logic Messages lost, delivered to wrong node, network flooding (loops), excessive latency. Relay node crashes.
  • Carefully debug relaying payload structure (final destination, original source).
  • Implement TTL for loop prevention.
  • Verify routing logic (how relay chooses next hop).
  • Log extensively at each relay step.
Ignoring Send Callback Status for Relayed Messages Original sender is unaware if a message failed at a subsequent relay hop. Data loss is silent. Relay node should check send status of forwarded message. If critical, implement application-level end-to-end ACKs or a mechanism for relay nodes to report failures upstream.
Broadcast Misunderstanding Broadcast messages not received by all desired nodes, or received by unexpected nodes. Remember ESP-NOW broadcast (peer_addr = NULL or broadcast MAC) sends to registered peers. It’s not a Wi-Fi beacon. Ensure intended recipients have the broadcaster added as a peer. Broadcasts are unencrypted and unacknowledged.
Device Discovery Issues New nodes cannot find existing nodes, or discovery is unreliable.
  • Ensure prober and discoverable nodes are on the same channel.
  • Verify broadcast probe messages are being sent correctly.
  • Ensure discoverable nodes are correctly parsing probes and sending unicast responses to the source MAC from recv_info.
  • Check for RF interference during discovery.

Exercises

  1. Star Network with Master Acknowledgment:
    • Modify Example 1 (Star Network). When the Master Node receives data from a Slave Node, it sends a short acknowledgment message back to that specific slave using unicast ESP-NOW. The slave should log the reception of this ACK.
  2. Bi-directional Relay:
    • Extend Example 2 (Simple Relay). Implement functionality so that Node C can send a reply message back to Node A, with Node B acting as the relay in the reverse direction as well. Node A should log the reply.
  3. Broadcast Control with Selective Action:
    • Master Node: Sends a broadcast ESP-NOW message containing a simple JSON string like {"target_group": 1, "command": "LED_ON"} or {"target_group": 2, "command": "LED_OFF"}.
    • Slave Nodes (at least 2): Each slave is assigned to a “group” (e.g., Slave 1 is group 1, Slave 2 is group 2). When a slave receives the broadcast, it parses the JSON. If the target_group matches its own group, it performs the command (e.g., prints “LED ON for Group X”).
    • This demonstrates using broadcast for commands but having application-level filtering on the payload.
  4. Dynamic Peer Addition via Serial Command:
    • Master Node: Starts with an empty peer list. It listens for serial input. If a user types “ADD_PEER,<MAC_ADDRESS>” via UART, the master adds that MAC as an ESP-NOW peer.
    • Slave Node: Sends data periodically to the master’s MAC address (which is hardcoded in the slave).
    • Test: Start master. Initially, it won’t receive from the slave. Use serial to add the slave’s MAC to the master. Then, the master should start receiving data. This simulates a basic form of dynamic peer configuration.

Summary

  • ESP-NOW can be used to create multi-device networks like star, line, or simple tree/mesh-like structures with application-layer relaying.
  • Effective peer management (esp_now_add_peer, esp_now_del_peer) is crucial, especially for central/relay nodes, keeping peer limits in mind.
  • ESP-NOW broadcast sends messages to all registered peers matching certain criteria; it’s not a generic, unlistened Wi-Fi broadcast. Broadcasts are unencrypted and unacknowledged individually.
  • Multi-hop communication requires custom application-level logic for addressing, routing, and data forwarding within the ESP-NOW payload.
  • Device discovery can be implemented using ESP-NOW broadcast probes and unicast responses.
  • All Wi-Fi capable ESP32 variants support these multi-device ESP-NOW networking techniques.

Further Reading

Leave a Comment

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

Scroll to Top