ESP-NOW

About this post

Update July 19, 2024: The board package for the ESP32 has a new version of the ESP-NOW library from version 3.0.0, which is not backwards compatible. I have adapted the sketches accordingly. The ESP8266 sketches further down in the article continue to work unchanged.

In my article WLAN with ESP8266 and ESP32, I showed how to let ESP boards communicate with each other via WiFi and how to control them via the browser of your PC or smartphone. However, if you only want to exchange data between two or more ESP boards, there is a simpler method called ESP-NOW.

I will explain step by step how to use your ESP32 or ESP8266 based boards with ESP-NOW as a receiver, transmitter or transceiver. I primarily use the ESP32 as a demonstration object. Finally, I will show you how to modify the code for ESP8266 boards.

This is the content:

Introduction / Preparations (ESP32 and ESP8266)

ESP-NOW is a protocol that allows you to exchange messages of up to 250 bytes between up to twenty ESP32 or ESP8266 boards. ESP-NOW is very flexible. It allows you to set up any board as a transmitter, receiver or transceiver and send messages to single or multiple members of your network. It is also possible to mix ESP32 and ESP8266 boards without any problems.

To use ESP-NOW, you do not need to install any additional software. The required libraries are part of the “standard equipment” of the ESP32 and ESP8266 packages.  

Getting the MAC address

The members of a network must be clearly identifiable. ESP-NOW uses the MAC address of your ESP board for this purpose. You can determine and format the MAC address with the following sketch:

#include "WiFi.h"
//#include <ESP8266WiFi.h> // for ESP8266 boards

void setup(){
    Serial.begin(115200);
    delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA); 
    while (!(WiFi.STA.started())) { // comment the while loop for ESP8266
        delay(10);
    }
    // delay(1000); // uncomment for ESP8266
    Serial.print("MAC-Address: ");
    String mac = WiFi.macAddress();
    Serial.println(mac);
    
    Serial.print("Formated: ");
    Serial.print("{");
    int index = 0;
    for(int i=0; i<6; i++){
        Serial.print("0x");
        Serial.print(mac.substring(index, index+2));
        if(i<5){
            Serial.print(", ");
        }
        index += 3;
    }
    Serial.println("}");
}
 
void loop(){}

The explicit setting of the Wi-Fi mode (line 7) is required from board package version 3.0.0 to determine the MAC address.

Here is an output example:

Output get_mac_address.ino
Output get_mac_address.ino

A few explanations about the sketch

To use the WiFi functions, you have to include WiFi.h for ESP32-based boards. For ESP8266-based boards, include ESP8266WiFi.h.

The MAC address consists of six bytes. Usually, the bytes are specified in the hexadecimal system, separated by colons. E.g. 94:3C:C6:33:68:98.

So, you have to find out the MAC address of every board that should participate in your ESP-NOW network and write it down. It is recommended to label the boards in some way to avoid confusion.

Note: If you load your sketches on ESP boards, then it may happen that the first Serial.print() instructions are “swallowed” during the first program run. A while(!Serial) after Serial.begin(), which is recommended for certain boards, does not eliminate the problem (if you should have it). That’s why you’ll find a commented out delay() in my sketches, which you can uncomment if necessary. 

Changing the MAC address

If it is too tedious for you to read out all MAC addresses, then you can simply set a new one. The following two sketches show how to do this for the ESP32 and the ESP8266.

ESP32

#include <WiFi.h>
#include <esp_wifi.h>

const uint8_t newMacAddress[] =  {0xC8, 0xC9, 0xA3, 0xC6, 0xFE, 0x54}; // customize as you wish  
uint8_t requestedNewMacAddress[6] = {0,0,0,0,0,0};

void setup(){
    Serial.begin(115200);
    delay(1000); 
    WiFi.mode(WIFI_STA);
    while (!(WiFi.STA.started())) {
        delay(100);
    }
    Serial.print("Default ESP Board MAC Address: ");
    Serial.println(WiFi.macAddress());
       
    esp_wifi_set_mac(WIFI_IF_STA, newMacAddress);
    esp_wifi_get_mac(WIFI_IF_STA, requestedNewMacAddress);
    
    Serial.print("New ESP Board MAC Address:     ");
    esp_wifi_get_mac(WIFI_IF_STA, requestedNewMacAddress);
    for (int i=0; i<6; i++) {
        Serial.print(requestedNewMacAddress[i],HEX);
        if (i<5) {
            Serial.print(":");
        }
    }   
}
 
void loop(){}

This is the output:

Output of change_mac_address.ino
Output of change_mac_address.ino

A few explanations:

  • We use a function from the ESP API (Application Programming Interface) in this example. You can recognize these functions by the fact that they start with “esp_”. To access them, you need to include the appropriate libraries. Here, it is esp_wifi.h.
  • The MAC address is defined as an array (uint8_t).
  • An ESP card can either be integrated into a network (station mode) or serve as an access point itself (access point mode). We set the station mode for ESP-NOW using WiFi.mode(WIFI_STA).
  • With esp_wifi_set_mac() you set the MAC address. The function expects two arguments. The first one is the interface used. Since we are working in Station mode, we select the station interface ( = WIFI_IF_STA). The second argument is the new MAC address.
    • The documentation for the esp_wifi_set_mac() function can be found here.
  • Bit 0 of the first byte of the MAC address must be 0, or in simpler terms: the first byte must be an even number. Otherwise, as far as I know, there are no other restrictions on your choice.
  • The change of the MAC address by esp_wifi_set_mac() is not permanent, i.e. the ESP “forgets” the setting at a reset.
  • Since the update to board package 3.x WiFi.macAddress() returns always the hardware MAC address. Therefore, here I applied inconvenient detour via esp_wifi_get_mac().

I will change the MAC address only in one of the example sketches. If you want to use this option in the other examples, you have to extend the sketches accordingly.

ESP8266

And this is how the counterpart for the ESP8266 looks like:

#include <ESP8266WiFi.h>

uint8_t newMacAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x01}; // customize as you wish

void setup(){
      Serial.begin(115200);
      // delay(1000); // uncomment if your serial monitor is empty
      Serial.print("Default ESP Board MAC Address: ");
      Serial.println(WiFi.macAddress());
      
      WiFi.mode(WIFI_STA);
      wifi_set_macaddr(STATION_IF, newMacAddress);
      
      Serial.print("New ESP Board MAC Address:     ");
      Serial.println(WiFi.macAddress());     
}
 
void loop(){}

Overview of the most important ESP-NOW functions (ESP32 and ESP8266)

To let your boards communicate via ESP-NOW, you only need a few functions, which I have summarized here in an overview:

The most important ESP-NOW functions at a glance
The most important ESP-NOW functions at a glance

The esp_now_register_recv_cb() function is the main reason why the version 3.x board package is not backwards compatible. More precisely, it is a change of the first function parameter from uint8_t * macAddr to const esp_now_recv_info* info. esp_now_recv_info is a struct which contains the following public members:

  • uint8_t *src_addr, i.e. the transmitter’s MAC address
  • uint8_t *des_addr, i.e. the receiver’s MAC address
  • wifi_pkt_rx_ctrl_t *rx_ctrl, this is RX control info

A complete list of functions can be found here in the API documentation for ESP-NOW.

One transmitter, one receiver – bare minimum (ESP32)

Since ESP-NOW can be a little confusing for beginners, we’ll start with a minimal example and then build on that. In this example, one board takes the role of the transmitter, the other serves as the receiver. The message consists of text only.

And again the hint: The code is ESP32 specific. I will come to the transfer to the ESP8266 at the end of the article.

Transmitter

#include <esp_now.h>
#include <WiFi.h>

uint8_t receiverAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x98};
esp_now_peer_info_t peerInfo;

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    esp_now_init();
    
    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    esp_now_add_peer(&peerInfo);
}
 
void loop(){
    char message[] = "Hi, this is a message from the transmitting ESP";
    esp_now_send(receiverAddress, (uint8_t *) message, sizeof(message)-1); // -1 to not send the NULL terminator
    delay(5000);
}

Explanations:

  • With #include<esp_now.h> we integrate the ESP-NOW library.
  • peerInfo contains information about the module we want to communicate with. This is a structure of type esp_now_peer_info_t. The documentation can be found here. For us, the following elements of the structure are relevant:
    • peer_addr contains the MAC address of the receiver module.
      • An assignment peerInfo.peer_addr = receiverAddress is not possible, because arrays cannot be copied so easily. Therefore, the detour via memcpy().
    • channel is the WiFi channel. You can choose the channels 1 – 13. With “0” the default setting takes effect, namely “1”. I will describe how to change the WLAN channel of your module at the end of the article.
    • encrypt specifies whether you want to encrypt the message. I am not going into that.
  • esp_now_init() initializes ESP-NOW.
  • esp_now_add_peer(&peerInfo) adds the ESP module you defined before with peerInfo to the network of the current module. 
  • esp_now_send() sends a message. You pass the MAC address of the recipient, the message and its length to the function. The message is of data type uint8_t and is passed as a pointer in esp_now_send(). Accordingly, message must still be explicitly converted using (uint8_t *).

Broadcasting

If you don’t want to determine MAC addresses, you could use the broadcast MAC address FF:FF:FF:FF:FF:FF instead of individual addresses. All receivers will then get the message, provided they are set to the same channel.

Receiver

#include <esp_now.h>
#include <WiFi.h>

void messageReceived(const esp_now_recv_info *info, const uint8_t* incomingData, int len){
    for(int i=0; i<len; i++){
        Serial.print((char)incomingData[i]);
    }
    Serial.println();
}


void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    esp_now_init();
    esp_now_register_recv_cb(messageReceived);
}

void loop(){}

Here are some explanations as well:

  • To receive data only, you do not need to add the sending module as a “peer”.
  • The only new function is esp_now_register_recv_cb(). With this, you register a function (here: messageReceived), which is called automatically when a message is received. The “cb” stands for “call back”. Since the function is called automatically, loop() can be left empty. You know the principle from interrupt service routines.
  • The parameters of the function to be called are the MAC address of the sender, the message itself and the length of the message.
  • Since we sent a text, but the received message is of data type uint8_t, we have to explicitly convert it back to char

Output on the serial monitor

If everything went well, then the message of the transmitter should be displayed on the serial monitor of the receiver module every five seconds.

Output of receiver_bare_minimum.ino

One transmitter, one receiver – advanced (ESP32)

The “bare minimum” sketches are not very convenient yet. There are no error messages if something goes wrong, no send confirmation, and no display of the MAC address of the transmitter. Also, we have only sent plain text so far. In practice, it is more likely that users will want to transmit data, such as sensor readings. We will now eliminate these deficits.

Transmitter sketch

First, the transmitter sketch:

#include <esp_now.h>
#include <WiFi.h>

uint8_t receiverAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x98};
esp_now_peer_info_t peerInfo;

typedef struct message {
    char text[64];
    int intVal;
    float floatVal;
} message;

message myMessage; 

void messageSent(const uint8_t *macAddr, esp_now_send_status_t status) {
    Serial.print("Send status: ");
    if(status == ESP_NOW_SEND_SUCCESS){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }
    
    esp_now_register_send_cb(messageSent);   

    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
}
 
void loop(){
    char textMsg[] = "Hi Receiver, here's my data for you: ";
    memcpy(&myMessage.text, textMsg, sizeof(textMsg));
    myMessage.intVal = 4242;
    myMessage.floatVal = 42.42;
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myMessage, sizeof(myMessage));
    if (result != ESP_OK) {
        Serial.println("Sending error");
    }
    delay(5000);
}

 

What is different compared to the bare minimum sketch?

  • As format for our message we use a structure (message). This gives us a lot of flexibility in terms of the types of data that are included in the message. In this example, the structure consists of a character array, an integer, and a float value. For the character array, we have to specify the maximum expected length.
  • Structures like message are kind of stripped down classes. message myMessage; creates the object myMessage.
  • The return value of esp_now_init() tells us whether the operation was completed without errors. 
  • In the same way, we use the return value of esp_now_add_peer(). Important: the function does not check whether the peer is really available or reachable. But, for example, you would get an error message if the maximum number of peers is exceeded. For more information, see the API documentation.
  • To copy the array textMsg to the array myMessage.text you have to use memcpy() for the reasons already mentioned before. You can assign the other elements directly. 
  • With esp_now_register_send_cb(messageSent); we register the function messageSent, which is called when a message has been sent. Again, the parameters are predefined.
  • Checking status in the function messageSent and checking result as the return value of esp_now_send() may seem like unnecessary duplication, but different criteria are checked in each case. For details – guess what – see the API documentation.

Receiver sketch

The receiver sketch can also be made more comfortable:

#include <esp_now.h>
#include <WiFi.h>

typedef struct message {
    char text[64];
    int intVal;
    float floatVal;
} message;

message myMessage;

void messageReceived(const esp_now_recv_info *info, const uint8_t* incomingData, int len){
    memcpy(&myMessage, incomingData, sizeof(myMessage));
    Serial.printf("Transmitter MAC Address: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
            info->src_addr[0], info->src_addr[1], info->src_addr[2], info->src_addr[3], info->src_addr[4], info->src_addr[5]);    
    Serial.print("Message: ");
    Serial.println(myMessage.text);
    Serial.print("Integer Value: ");
    Serial.println(myMessage.intVal);
    Serial.print("Float Value: ");
    Serial.println(myMessage.floatVal);
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){}

 

What is different here compared to the bare minimum sketch?

  • We implement the same structure message as in the transmitter sketch.
  • We copy the incoming message to myMessage using memcpy() and then we can conveniently access the elements of the structure myMessage.
  • We display the MAC address of the transmitter. The formatting (hex numbers, colons) is provided by the printf() function.  
    • The MAC address (src_addr) is a member of info. Since info is passed to messageReceived() as a pointer, we have to use the arrow instead of the dot operator to access src_addr.

Output of receiver_basic.ino

Output of receiver_basic.ino
Output of receiver_basic.ino

Several transmitters, one receiver (ESP32)

In practice, one often uses several transmitters and one receiver. A typical application example would be a weather station with sensors at different locations that transmit their data to a central station.

You do not need any additional ESP-NOW functions for this configuration. The only challenge is the assignment of the incoming data to the transmitters. The obvious solution would be to match the transmitter MAC address with a list stored in the receiver sketch. However, an identity check of arrays is quite computationally intensive. Alternatively, the message from the transmitter could contain an identifier.  

I decided to use a different method. All ESP modules participating in the network get a new MAC address. The MAC addresses differ only in the last byte, which also serves as numbering. In my example, three transmitters are used, and I have assigned the following MAC addresses for them:

  • Transmitter 0 address = 94:3C:C6:33:68:00,
  • Transmitter 1 address = 94:3C:C6:33:68:01,
  • Transmitter 2 address = 94:3C:C6:33:68:02,
  • Receiver address = 94:3C:C6:33:68:05

Transmitter sketch example

Here is the sketch for the transmitter 0:

#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>

uint8_t receiverAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x05};
uint8_t myAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x00};
esp_now_peer_info_t peerInfo;

typedef struct data {
    int humidity;
    float temperature;
} data;

data myMessage; 

void messageSent(const uint8_t *macAddr, esp_now_send_status_t status) {
    Serial.print("Send status: ");
    if(status == ESP_NOW_SEND_SUCCESS){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void setup(){
    Serial.begin(115200);
    delay(1000);
    WiFi.mode(WIFI_STA);
    esp_wifi_set_mac(WIFI_IF_STA, myAddress);
    Serial.print("New ESP Board MAC Address:  ");
    Serial.println(WiFi.macAddress());  
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }
    
    esp_now_register_send_cb(messageSent);   

    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
}
 
void loop(){
    myMessage.humidity = 42;
    myMessage.temperature = 16.9;
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myMessage, sizeof(myMessage));
    if (result != ESP_OK) {
        Serial.println("Sending error");
    }
    delay(3000);
}

 

As data we send the humidity and the temperature, which we could have determined with a DHT22, for example. The data is “packed” in the structure data.

Receiver

On the receiver side, we first copy the incoming messages to the “auxiliary structure” stationMsg and from there we transfer them to the array weatherStation[]. The counter for the elements of the array is simply the last digit of the MAC address, i.e. info->src_addr[5]:

#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>

uint8_t myAddress[] = {0x94, 0x3C, 0xC6, 0x33, 0x68, 0x05};

typedef struct data {
    int humidity;
    float temperature;
} data;

data stationMsg;
data weatherStation[3] = {0, 0};

void messageReceived(const esp_now_recv_info *info, const uint8_t* incomingData, int len){
    memcpy(&stationMsg, incomingData, sizeof(stationMsg));
    weatherStation[info->src_addr[5]].humidity = stationMsg.humidity;
    weatherStation[info->src_addr[5]].temperature = stationMsg.temperature;
}

void setup(){
    Serial.begin(115200);
    delay(1000);
    WiFi.mode(WIFI_STA);
    esp_wifi_set_mac(WIFI_IF_STA, myAddress);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){
    for(int i=0; i<3; i++){
        Serial.print("Weather Station ");
        Serial.print(i);
        Serial.println(":");
        Serial.print("Humidity [%]    : ");
        Serial.println(weatherStation[i].humidity);
        Serial.print("Temperature [°C]: ");
        Serial.println(weatherStation[i].temperature,1);
        Serial.println();
    }
    Serial.println();
    delay(5000);   
}

 

Here is the output:

Output of multi_transm_one_recv_receiver.ino
Output of multi_transm_one_recv_receiver.ino

One transmitter, multiple receivers (ESP32)

It is just as easy to network one transmitter and multiple receivers. This configuration could be used in smart home applications, for example. The sender must know all the MAC addresses of the peers, i.e. the receivers, and connect to all of them.

For simplicity and better overview, in my example all receivers get sent the same data types, so we only need to define one structure. If necessary, this could be easily changed.

Transmitter

#include <esp_now.h>
#include <WiFi.h>

uint8_t receiverAddress[3][6] =  {{0xC8, 0xC9, 0xA3, 0xCA, 0x22, 0x70},
                                  {0xC8, 0xC9, 0xA3, 0xC6, 0xFE, 0x54},
                                  {0x94, 0xE6, 0x86, 0x0D, 0x7B, 0x80}};
esp_now_peer_info_t peerInfo[3];

typedef struct message {
    char text[32];
    int intVal;
    float floatVal;
} message;

message myMessage[3]; 

void messageSent(const uint8_t *macAddr, esp_now_send_status_t status) {
    Serial.printf("Send status to receiver %02X:%02X:%02X:%02X:%02X:%02X : ", 
            macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);        
    if(status == ESP_NOW_SEND_SUCCESS){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }
    
    esp_now_register_send_cb(messageSent);   

    for(int i=0; i<3; i++){
        memcpy(peerInfo[i].peer_addr, receiverAddress[i], 6);
        peerInfo[i].channel = 0;
        peerInfo[i].encrypt = false;

        if (esp_now_add_peer(&peerInfo[i]) != ESP_OK) {
            Serial.println("Failed to add peer");
            return;
        }
    }
}
 
void loop(){
    char textMsg0[] = "Hi Receiver 0";
    memcpy(&myMessage[0].text, textMsg0, sizeof(textMsg0));
    myMessage[0].intVal = 4242;
    myMessage[0].floatVal = 42.42;
    
    char textMsg1[] = "Ciao Receiver 1";
    memcpy(&myMessage[1].text, textMsg1, sizeof(textMsg1));
    myMessage[1].intVal = 1234;
    myMessage[1].floatVal = 12.34;
    
    char textMsg2[] = "Hola Receiver 2";
    memcpy(&myMessage[2].text, textMsg2, sizeof(textMsg2));
    myMessage[2].intVal = 4711;
    myMessage[2].floatVal = 47.11;
    
    for(int i=0; i<3; i++){
        esp_err_t result = esp_now_send(receiverAddress[i], (uint8_t *) &myMessage[i], sizeof(myMessage[i]));
        if (result != ESP_OK) {
            Serial.print("Sending error module ");
            Serial.println(i);
        }
    }
     delay(10000);
}

 

I don’t think the code needs any further explanation, do you?

You could also simplify this sketch a bit by changing the MAC addresses of the receivers so that they only differ in the last byte. You then only need a one-dimensional array for the receiverAddress, and if you want to address receiver no. i, then you only have to set the address with receiverAddress[5] = 0x0i.

Maybe also useful: If you replace the receiver address in the function esp_now_send() by NULL (written exactly like this, not “0”), the message will be sent to all registered peers.

Output

This is what the output looks like on the transmitter side:

Output of one_transm_multi_recv_transmitter.ino

Receiver

The sketch for the receivers should also be understandable without further explanation:

#include <esp_now.h>
#include <WiFi.h>

typedef struct message {
    char text[32];
    int intVal;
    float floatVal;
} message;

message myMessage;

void messageReceived(const esp_now_recv_info *info, const uint8_t* incomingData, int len){
    memcpy(&myMessage, incomingData, sizeof(myMessage));
    Serial.print("Message: ");
    Serial.println(myMessage.text);
    Serial.print("Integer Value: ");
    Serial.println(myMessage.intVal);
    Serial.print("Float Value: ");
    Serial.println(myMessage.floatVal);
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){}

 

Output

And here still exemplarily the output of Receiver 1:

Output of one_transm_multi_recv_receiver.ino
Output of one_transm_multi_recv_receiver.ino

ESP as transceiver (ESP32)

One setup is still missing, namely the use of ESP boards as transceivers, i.e. as combined transmitters and receivers. The good news is that you don’t need any new features to do this. You also don’t have to switch between receive and transmit mode or similar. Just take one of the transmitter sketches and add the elements needed for a receiver (or the other way around).

In my example of this, one transceiver (the “lead”) sends data to another transceiver (the “follower”) at certain intervals, which thanks the lead transceiver for doing so and reports back its runtime in seconds. Admittedly, it makes little sense, but it is only meant to illustrate the principle!

I named the lead transceiver because it sets the pace in this specific example. Just as well, the two transceivers could send their messages to each other on an equal and event-based basis, e.g. at certain time intervals, when a certain sensor value limit is reached, when a button is pressed, or whatever comes to your mind.

Sketch for the “lead transceiver”

#include <esp_now.h>
#include <WiFi.h>

uint8_t receiverAddress[] =  {0xC8, 0xC9, 0xA3, 0xCA, 0x22, 0x70};
esp_now_peer_info_t peerInfo;

typedef struct messageToBeSent {
    char text[64];
    int intVal;
    float floatVal;
} messageToBeSent;

typedef struct receivedMessage {
    char text[64];
    long runTime;
} receivedMessage;

messageToBeSent myMessageToBeSent; 
receivedMessage myReceivedMessage; 

void messageSent(const uint8_t *macAddr, esp_now_send_status_t status) {
    Serial.print("Send status: ");
    if(status == ESP_NOW_SEND_SUCCESS){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void messageReceived(const esp_now_recv_info *info, const uint8_t* incomingData, int len){
    memcpy(&myReceivedMessage, incomingData, sizeof(myReceivedMessage));
    Serial.printf("Incoming Message from: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
            info->src_addr[0], info->src_addr[1], info->src_addr[2], info->src_addr[3], info->src_addr[4], info->src_addr[5]);
    Serial.print("Message: ");
    Serial.println(myReceivedMessage.text);
    Serial.print("RunTime [s]: ");
    Serial.println(myReceivedMessage.runTime);
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }
    
    esp_now_register_send_cb(messageSent);  
    esp_now_register_recv_cb(messageReceived); 

    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
}
 
void loop(){
    char textMsg[] = "Hi, here's my data for you: ";
    memcpy(&myMessageToBeSent.text, textMsg, sizeof(textMsg));
    myMessageToBeSent.intVal = 4242;
    myMessageToBeSent.floatVal = 42.42;
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myMessageToBeSent, sizeof(myMessageToBeSent));
    if (result != ESP_OK) {
        Serial.println("Sending error");
    }
    delay(5000);
}

 

As you can see – it’s just a combination of transmitter and receiver sketches.

Sketch for the “Following Transceiver”

Here, on the follower side, the messages are received and acknowledged with a reply.

#include <esp_now.h>
#include <WiFi.h>

uint8_t receiverAddress[] = {0x94, 0x3C, 0xC6, 0x34, 0xCF, 0xA4};
esp_now_peer_info_t peerInfo;

typedef struct messageToBeSent{
    char text[64];
    long runTime;
} messageToBeSent;

typedef struct receivedMessage {
    char text[64];
    int intVal;
    float floatVal;
} receivedMessage;

messageToBeSent myMessageToBeSent; 
receivedMessage myReceivedMessage; 

void messageReceived(const esp_now_recv_info *info, const uint8_t* incomingData, int len){
    memcpy(&myReceivedMessage, incomingData, sizeof(myReceivedMessage));
    Serial.printf("Incoming Message from: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
                info->src_addr[0], info->src_addr[1], info->src_addr[2], info->src_addr[3], info->src_addr[4], info->src_addr[5]);
    Serial.print("Message: ");
    Serial.println(myReceivedMessage.text);
    Serial.print("Integer Value: ");
    Serial.println(myReceivedMessage.intVal);
    Serial.print("Float Value: ");
    Serial.println(myReceivedMessage.floatVal);
    Serial.println();
    Serial.println("Sending answer...");
    Serial.println(); 

    char textMsg[] = "Thanks for the data!";
    memcpy(&myMessageToBeSent.text, textMsg, sizeof(textMsg));
    myMessageToBeSent.runTime = millis()/1000;
    esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &myMessageToBeSent, sizeof(myMessageToBeSent));
    if (result != ESP_OK) {
        Serial.println("Sending error");
    }    
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == ESP_OK) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_register_recv_cb(messageReceived);
    
    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;

    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Failed to add peer");
        return;
    }
}
 
void loop(){}

 

Outputs:

Here is the output for the lead:

Output of transceiver_lead.ino

And here is the output for the follower:

Output of transceiver_follower.ino

I will not show an example of networking multiple transceivers. You should be able to deduce this easily from the previous examples.

New festures from board package version 3.0.0

The ESP-NOW library from ESP32 board package version 3.0.0 has been given some very convenient functions. Try out the example sketches ESP_NOW_Broadcast_Master, ESP_NOW_Broadcast_Slave and ESP_NOW_Networking (file → Examples → ESP_NOW). With the help of these sketches, you can set up ESP-NOW networks, in which the members are registered automatically. So, you don’t have to determine MAC addresses “manually”. However, these sketches are not necessarily easy to understand for beginners. Perhaps I will write a separate article on this topic.

Another new feature is ESP-NOW Serial. This feature is not as convenient, but impresses with its simple operation. There is also an example sketch available. I will report on ESP-NOW Serial in one of my next articles.

“Translation” of the sketches for the ESP8266

The ESP8266 implementation of ESP-NOW is not very different from the implementation on the ESP32. However, the difference is large enough that unified sketches (via annotations or #ifdef ESP32...#else constructions) would have become confusing. 

Here are the main differences:

  • The names of the libraries to be included.
  • The parameters of the callback functions. 
  • For the ESP82866, its role as receiver, transmitter or transceiver must be explicitly defined with esp_now_set_self_role().
  • Return values of some functions.
  • The first parameter of the function that is declared using esp_now_register_recv_cb() (here: messageReceived()) is uint8_t* macAddr instead of const esp_now_recv_info* info.

As an example I have “translated” the transceiver sketches for the ESP8266.

Lead

#include <espnow.h>
#include "ESP8266WiFi.h"

uint8_t receiverAddress[] = {0xA4, 0xCF, 0x12, 0xDF, 0x5D, 0x89};

typedef struct messageToBeSent {
    char text[64];
    int intVal;
    float floatVal;
} messageToBeSent;

typedef struct receivedMessage {
    char text[64];
    long runTime;
} receivedMessage;

messageToBeSent myMessageToBeSent; 
receivedMessage myReceivedMessage; 

void messageSent(uint8_t *macAddr, uint8_t status) {
    Serial.print("Send status: ");
    if(status == 0){
        Serial.println("Success");
    }
    else{
        Serial.println("Error");
    }
}

void messageReceived(uint8_t* macAddr, uint8_t* incomingData, uint8_t len){
    memcpy(&myReceivedMessage, incomingData, sizeof(myReceivedMessage));
    Serial.printf("Incoming Message from: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
            macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);    
    Serial.print("Message: ");
    Serial.println(myReceivedMessage.text);
    Serial.print("RunTime [s]: ");
    Serial.println(myReceivedMessage.runTime);
    Serial.println();
}

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == 0) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
    uint8_t result = esp_now_add_peer(receiverAddress, ESP_NOW_ROLE_COMBO, 0, NULL, 0);
    if(result != 0){
        Serial.println("Failed to add peer");
    }
    
    esp_now_register_send_cb(messageSent);  
    esp_now_register_recv_cb(messageReceived); 

   
}
 
void loop(){
    char textMsg[] = "Hi, here's my data for you: ";
    memcpy(&myMessageToBeSent.text, textMsg, sizeof(textMsg));
    myMessageToBeSent.intVal = 4242;
    myMessageToBeSent.floatVal = 42.42;
    esp_now_send(receiverAddress, (uint8_t *) &myMessageToBeSent, sizeof(myMessageToBeSent));
    delay(5000);
}

 

Followers

#include <espnow.h>
#include "ESP8266WiFi.h"

uint8_t receiverAddress[] = {0x44, 0x17, 0x93, 0x0E, 0x2E, 0xED};

typedef struct messageToBeSent{
    char text[64];
    long runTime;
} messageToBeSent;

typedef struct receivedMessage {
    char text[64];
    int intVal;
    float floatVal;
} receivedMessage;

messageToBeSent myMessageToBeSent; 
receivedMessage myReceivedMessage; 

void messageReceived(uint8_t* macAddr, uint8_t* incomingData, uint8_t len){
    memcpy(&myReceivedMessage, incomingData, sizeof(myReceivedMessage));
    Serial.printf("Incoming Message from: %02X:%02X:%02X:%02X:%02X:%02X \n\r", 
            macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);
    Serial.print("Message: ");
    Serial.println(myReceivedMessage.text);
    Serial.print("Integer Value: ");
    Serial.println(myReceivedMessage.intVal);
    Serial.print("Float Value: ");
    Serial.println(myReceivedMessage.floatVal);
    Serial.println();
    Serial.println("Sending answer...");
    Serial.println(); 

    char textMsg[] = "Thanks for the data!";
    memcpy(&myMessageToBeSent.text, textMsg, sizeof(textMsg));
    myMessageToBeSent.runTime = millis()/1000;
    esp_now_send(receiverAddress, (uint8_t *) &myMessageToBeSent, sizeof(myMessageToBeSent));    
}    

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    
    if (esp_now_init() == 0) {
        Serial.println("ESPNow Init success");
    }
    else {
        Serial.println("ESPNow Init fail");
        return;
    }

    esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
    uint8_t result = esp_now_add_peer(receiverAddress, ESP_NOW_ROLE_COMBO, 0, NULL, 0);
    if(result != 0){
        Serial.println("Failed to add peer");
    }
    
    esp_now_register_recv_cb(messageReceived);
}
 
void loop(){}

 

Mixing ESP32 and ESP826 boards

This is (now?) no problem (anymore). Contrary to my previous statements, ESP32 and ESP8266 boards can communicate with each other without any problems. Either something has changed in the ESP-NOW libraries or I did something stupid in my previous attempts. I have verified this using the transceiver sketches on an ESP32 development board and a Wemos D1 Mini.

Changing channels (ESP32 and ESP8266)

If you have range problems due to interaction with other WiFi networks, then switching to a different channel might help. The following two sketches show how to change the channel for an ESP32 and an ESP8266 board. All participants in a network must of course be set to the same channel.

ESP32

#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#define CHANNEL 13

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    Serial.print("WiFi-Channel Default: ");
    Serial.println(WiFi.channel());
    
    esp_wifi_set_promiscuous(true);
    esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE);
    esp_wifi_set_promiscuous(false);
    
    Serial.print("WiFi-Channel Update : ");
    Serial.println(WiFi.channel());
}
     
void loop(){}

ESP8266

#include <espnow.h>
#include "ESP8266WiFi.h"
#define CHANNEL 13

void setup(){
    Serial.begin(115200);
    // delay(1000); // uncomment if your serial monitor is empty
    WiFi.mode(WIFI_STA);
    Serial.print("WiFi-Channel Default: ");
    Serial.println(WiFi.channel());
    
    wifi_promiscuous_enable(true);
    wifi_set_channel(CHANNEL);
    wifi_promiscuous_enable(false); 

    Serial.print("WiFi Channel Update : ");
    Serial.println(WiFi.channel());
}
 
void loop(){}

Range

If you google for range tests for ESP-NOW, you will come across maximum values between 100 and 250 meters, as long as the tests were conducted outdoors, with a free line of sight and without external antennas. How far you get in your apartment or house depends mainly on the building fabric.

In my own range tests, two Wemos D1 Mini Boards and two ESP32-WROOM-32 based development boards were used. Both pairs achieved identical ranges. In the next room, i.e. through one wall, I did not lose any messages. One room further, i.e. through two walls, I had good connection only in some areas. This is roughly comparable to the range of my Fritz!Box 7590.

If the range is not sufficient for you, you could place a module as a repeater between the modules to be connected. Or you can use modules that allow you to connect external antennas.

Acknowledgement

I have S K on Pixabay to thank for the note with the “NOW” on the post image.

11 thoughts on “ESP-NOW

  1. Hi,
    I also have to say that this article is one of the best I have found so far – great work!

    I was looking for more information about power saving. I have used deep sleep in the past with success, but that seems not to work with ESPNow. There is some information in the IDF docomentation with some functions like esp_now_set_wake_window() and esp_wifi_connectionless_module_set_wake_interval() but I couldn’t find any examples for these.

    Have you looked into this?

    Best regards

    Uli

    1. Hi Uli,
      thanks for the kind feedback. As far as I know WiFi is no wake-up source for deep sleep, so at least I can confirm that this is no option. Unfortunately, I have no experience with the functions you mentioned. Going deeper into the API functions is still on my To-Do list. I have searched a bit and found this:
      https://github.com/espressif/esp-idf/issues/5044
      Not sure if this helps.
      Best wishes, Wolfgang

    2. Hi there,

      Very useful article! I use the deepsleep() option of the ESP:
      Serial.printf(“Delta t2-t1: %d (ms)\nEntering deep sleep\n”, t2 – t1); //delta T processing of transmission
      ESP.deepSleep(10000000, WAKE_RFCAL);

      NOTE: connect GPIO16 with RESET pin in order to enable esp.deepsleep() to function.

  2. Good morning,
    Thank you very much for this clear site, this magnificent article with clear and pragmatic explanations. Without a doubt one of the best articles I have read on the subject of espnow.
    If you decide to continue using espnow and wifi server on the same esp8266 with automatic channel change management, I will enjoy your article. Thank you again.

  3. Nice one. Thanks for your update.

    I modified your SPI code to send via ESPnow, but it is not working. What have I missed?

    #include
    #include
    #include
    #include

    #define CS_PIN 5 // Chip Select Pin
    bool spi = true; // flag that SPI shall be used

    #define CHANNEL 1

    // Replace with your receiver MAC address
    uint8_t broadcastAddress[] = { 0x13, 0x93, 0xC5, 0xB6, 0x69, 0x92 };

    ADXL345_WE myAcc = ADXL345_WE(CS_PIN, spi);
    // Create a struct to hold accelerometer data
    typedef struct struct_message {
    int id;
    float x;
    float y;
    float z;

    } struct_message;

    struct_message myData;
    esp_now_peer_info_t peerInfo;

    //callback when data is sent
    void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
    Serial.print(“\r\nLast Packet Send Status:\t”);
    Serial.println(status == ESP_NOW_SEND_SUCCESS ? “Delivery Success” : “Delivery Fail”);
    }

    void setup() {
    Serial.begin(2000000);
    Serial.println(“ADXL345_Sketch – Basic Data”);
    if (!myAcc.init()) {
    Serial.println(“ADXL345 not connected!”);
    while (1)
    ;
    }
    WiFi.mode(WIFI_STA);
    if (esp_now_init() != ESP_OK) {
    Serial.println(“Error”);
    return;
    }
    esp_now_register_send_cb(OnDataSent);
    memcpy(peerInfo.peer_addr, broadcastAddress, 6);
    peerInfo.channel = 0;
    peerInfo.encrypt = false;
    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
    Serial.println(“Failed to add peer”);
    return;
    }
    myAcc.setDataRate(ADXL345_DATA_RATE_1600);
    myAcc.setRange(ADXL345_RANGE_2G);
    }

    void loop() {

    xyzFloat raw = myAcc.getRawValues();
    xyzFloat g = myAcc.getGValues();

    myData.id = 1;

    esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *)&myData, sizeof(myData));
    Serial.print(g.x);
    Serial.print(“,”);
    Serial.print(g.y);
    Serial.print(“,”);
    Serial.println(g.z);
    }

    1. Hi, it’s always important to know what “not working” means. Compilation error? Error message when sending? Nothing received? Etc.

      But what I can immediately say is that you need to assign the measured values to myData:
      myData.x = g.x;
      myData.y = g.y;
      myData.z = g.z;

      1. Thank you very much; you are a guru. It is working perfectly.
        Can you further advise me on measuring the delay or latency between the two ESPs?

        1. Nice to hear that it works.

          In theory the data rate is 1 Mbps. But this is the pure rate while sending. There’s additional time need to prpare the sending and read the received message from the buffer. I have not done measurements for the effective rate. You may want to have a look here:

          https://github.com/espressif/esp-idf/issues/3238

Leave a Reply

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