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)
- Overview of the most important ESP-NOW functions (ESP32 and ESP8266)
- One transmitter, one receiver – bare minimum (ESP32)
- One transmitter, one receiver – advanced (ESP32)
- Several transmitters, one receiver (ESP32)
- One transmitter, multiple receivers (ESP32)
- ESP boards as transceivers (ESP32)
- New features from board package version 3.0.0
- “Translation” of the sketches for the ESP8266
- Mixing ESP32 and ESP826 boards
- Changing channels (ESP32 and ESP8266)
- Range
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:
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:
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 viaesp_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 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 addressuint8_t *des_addr
, i.e. the receiver’s MAC addresswifi_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 typeesp_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 viamemcpy()
.
- An assignment
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 withpeerInfo
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 typeuint8_t
and is passed as a pointer inesp_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 tochar
.
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.
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 objectmyMessage
. - 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 arraymyMessage.text
you have to usememcpy()
for the reasons already mentioned before. You can assign the other elements directly. - With
esp_now_register_send_cb(messageSent);
we register the functionmessageSent
, which is called when a message has been sent. Again, the parameters are predefined. - Checking
status
in the functionmessageSent
and checkingresult
as the return value ofesp_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
usingmemcpy()
and then we can conveniently access the elements of the structuremyMessage
. - 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 ofinfo
. Sinceinfo
is passed tomessageReceived()
as a pointer, we have to use the arrow instead of the dot operator to accesssrc_addr
.
- The MAC address (
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:
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:
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:
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:
And here is the output for the follower:
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()
) isuint8_t* macAddr
instead ofconst 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.
Hello
I use ESPNOW with ESP32s since 1 week with google
Your publication is the BEST : structured, very good explanations, uptodate with version 3, working example, …
I try to measure RSSI and find only this code (interface configured in promiscuous mode)
https://github.com/TenoTrash/ESP32_ESPNOW_RSSI/blob/main/Modulo_Receptor_OLED_SPI_RSSI.ino
Give Espressif access to RSSI level in ESPNOW API ?
Encore BRAVO
Gérald retired IT professor
Hello,
thank you so much for your kind feedback! Unfortunately, it seems there is no easy way to get the RSSI level. A simple function like “getRSSI()” or so would be great. I looked for this in the ESP NOW API:
https://docs.espressif.com/projects/arduino-esp32/en/latest/api/espnow.html
…but could not find anything. Sorry!
Best wishes, Wolfgang
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
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
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.
Thanks for your feedback and the useful hint!
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.
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);
}
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;
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?
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