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:
- 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.
- 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.
- 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
- 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.
- Broadcast:
- Sending a message using
esp_now_send(NULL, ...)
oresp_now_send(broadcast_mac, ...)
wherebroadcast_mac
isFF:FF:FF:FF:FF:FF
. - Sender Behavior: If
peer_addr
isNULL
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.
- Sending a message using
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 |
|
|
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:
- 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.
- 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).
- Node B receives the message from A. Its application logic inspects the payload.
- Node B sees that the message is for Node C. Node B adds Node C as a peer (if not already added).
- Node B retransmits the original payload (or a modified one if needed) to Node C.
- 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). |
|
Routing Logic | How does a relay node determine the next hop for a given final destination, especially if multiple paths exist? |
|
Loop Prevention | Messages could be relayed in circles in networks with redundant paths, consuming bandwidth and resources. |
|
Latency | Each relay hop introduces processing and transmission delay. Overall latency increases with the number of hops. |
|
Reliability | Each hop is a point of potential failure (node down, RF interference). ESP-NOW ACKs are only hop-to-hop. |
|
Increased Complexity | Application code on relay nodes becomes more complex than simple end-nodes. |
|
Power Consumption | Relay nodes are always on (or wake frequently) to listen and forward, increasing their power consumption. |
|
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:
- 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.
- 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. - 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):
#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):
#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:
- Get MAC addresses of all three ESP32s.
- Update
s_slave1_mac
,s_slave2_mac
inmaster_node_main.c
. - Update
s_master_mac
inslave1_main.c
andslave2_main.c
. - Ensure
WIFI_CHANNEL
is the same for all. - Build and flash each project to its respective ESP32.
- Open serial monitors for all three.
- 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.
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):
// ... (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):
// ... (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):
// ... (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:
- Update all MAC addresses in all three projects.
- Flash each project to a separate ESP32.
- Ensure all are on the same
WIFI_CHANNEL
. - 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
andCONFIG_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. |
|
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. |
|
Exercises
- 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.
- 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.
- 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.
- Master Node: Sends a broadcast ESP-NOW message containing a simple JSON string like
- 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
- ESP-IDF Programming Guide – ESP-NOW: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/network/esp_now.html (This remains the primary official documentation).
- Espressif IoT Solution – ESP-MESH: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-guides/esp-wifi-mesh.html – While this chapter is about ESP-NOW networks, if you need true self-healing, self-routing mesh capabilities, ESP-MESH (which is built on top of Wi-Fi) is Espressif’s dedicated solution and worth exploring as an alternative for more complex mesh requirements.
- Community Projects and Blogs: Searching for “ESP-NOW sensor network,” “ESP-NOW relay,” or “ESP-NOW mesh” may reveal community projects and blog posts where developers share their experiences and custom solutions for larger ESP-NOW deployments.