ESP32: Getting started with ESP-NOW

In this tutorial we will check how to get started with ESP-NOW, a connectionless WiFi communication protocol from Espressif [1]. For this tutorial two ESP32 devices were used: a FireBeetle board and a Beetle board, both from DFRobot.

Introduction

In this tutorial we will check how to get started with ESP-NOW, a connectionless WiFi communication protocol from Espressif [1]. You can read more about the protocol here and you can check the documentation from IDF here.

We will be using the Arduino core. Nonetheless, since no higher level wrappers exist on the Arduino core for ESP-NOW, we will need to use the lower level IDF functions, as we will see below.

Our introductory example will consist on a device (the sender) broadcasting a simple message containing an integer. Then, another device (the receiver) will receive and print the message to the serial port.

For this tutorial two ESP32 devices were used: a FireBeetle board and a Beetle board, both from DFRobot.

The sender code

We will start the code by including the libraries we need to work with ESP-NOW. We will need the esp_now.h, which will expose the functions related with the protocol.

We will also need the WiFi.h library, so we can setup if the device will be working on soft AP or station mode (both can be used with ESP-NOW [1]).

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

The protocol supports both unicast and multicast communication. In our case, we are going to broadcast a message to all the devices, so we will define a variable with the MAC broadcast address.

uint8_t broadcastAddress[] = {0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF};

Moving on to the Arduino setup, we will start by opening a serial connection. Then, we will setup the WiFi mode to station, with a call to the mode method on the WiFi extern variable.

Note that, under the hood, this mode method call will initialize WiFi if it is not initialized yet. We need to do this initialization first, before we initialize ESP-NOW [1].

Serial.begin(115200);

WiFi.mode(WIFI_STA);

We will print the MAC address of the sender, so we can check later, on the receiver, if the message came from the device we are expecting. We will have access to the sender’s MAC address on the receiver, as we will see later.

Serial.println(WiFi.macAddress());

After this, to initialize ESP-NOW, we simply need to call the esp_now_init function. This function takes no arguments and returns as output a value of type esp_err_t.

We will use the return value to confirm the initialization was successful. In that case, the returned value should be equal to ESP_OK.

if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
}

Before we send data to another device, we need to add it to the paired device list first [1]. Using a nomenclature closer to the functions used in the documentation, it means that we are going to add a peer.

Even in our case (we want to send a broadcast message) we need to add a peer with the broadcast address [1].

To hold the peer information, we will need a struct of type esp_now_peer_info_t. We are going to need to set the following data members:

  • peer_addr: address of the peer device. We will set it to the broadcast address.
  • channel: Wi-Fi channel that the peer uses to send/receive data. We will set it to 0.
  • encrypt: indicates if the data that the peer sends / receives is encrypted or not. For this example, we will set it to false.
esp_now_peer_info_t peerInfo;
  
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;  
peerInfo.encrypt = false;

To add the peer to the list, we simply need to call the esp_now_add_peer function, passing as input the address of our peer information struct.

As output, this function returns a value of type esp_err_t, which we will also use for error check.

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

After successfully registering the peer, we will broadcast the data. Our message will be a simple integer, but we can use other data structures as long as the total size doesn’t exceed 250 bytes [1]. So, we will define an integer with the value 10.

int x = 10;

To send the actual data, we need to call the esp_now_send function.

As first input, this function receives the address of the peer to which we want to send the data. If we pass NULL to this argument, data is sent to all the peers from the list. In our case, we will pass the broadcast address, since we already have a peer with that address.

As second input, we need to pass the address of the data to be sent. Since this parameter needs to be of type uint8_t *, we need to do a cast for this type, since the variable that holds our data is of type int (so it’s address is of type int *).

As third and last input we need to pass the length of the data we are going to send.

As output, the function will return a value of type esp_err_t, which we will store in a variable and use for error checking.

esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &x, sizeof(int));
    
if (result == ESP_OK) {
    Serial.println("Sent with success");
}
else {
    Serial.println("Error sending the data");
}

With the previous call we finish our setup function. The final complete code can be seen below. We don’t need to add anything to the main loop since, for this example, we will send the data only once,

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

uint8_t broadcastAddress[] = {0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF};

void setup() {

  Serial.begin(115200);

  WiFi.mode(WIFI_STA);

  Serial.println();
  Serial.println(WiFi.macAddress());

  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // register peer
  esp_now_peer_info_t peerInfo;
  
  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;
  }

  // send data
  int x = 10;
    
  esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &x, sizeof(int));
    
  if (result == ESP_OK) {
    Serial.println("Sent with success");
  }
  else {
    Serial.println("Error sending the data");
  }

}

void loop() {}

The receiver code

Like we did before, we will need to include the esp_now.h and the WiFi.h libraries.

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

In the setup function, we will start by opening a serial connection, to output the results of our program. After that, we will set the WiFi mode to station, like we also did on the sender.

Serial.begin(115200);

WiFi.mode(WIFI_STA);

Then we will initialize the ESP-NOW with a call to the esp_now_init, also like we did on the sender.

if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
}

To finalize, we will register a callback function that will be executed when data is received. We do this by calling the esp_now_register_recv_cb and passing as input our callback function (we will define it later). We will call this callback function onReceiveData.

The full setup function can be seen below and already contains the registering of the function.

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);

  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  esp_now_register_recv_cb(onReceiveData);
}

Now we will look into the definition of the callback function. This function needs to follow a pre-defined signature: it must return void and receive as input the following 3 parameters:

  • An array of type uint8_t with the MAC address of the peer that sent the data;
  • A pointer to the received data;
  • The length of the received data.

Note that this callback function runs from the WiFi task, which means we should not do lengthy operations on it [1]. For simplicity, we will print the received data and the MAC of the sender from this callback function but, in a real application scenario,it is recommended that, for example, we post the data to a queue and handle it from a lower priority task [1].

void onReceiveData(const uint8_t *mac, const uint8_t *data, int len) {
    // callback function code
}

In the implementation of our handling function we will first iterate through all the 6 bytes of the MAC address and print them to the serial port. Note that this address should match the one from the sender (that we printed on its setup function).

for (int i = 0; i < 6; i++) {

    Serial.printf("%02X", mac[i]);
    if (i < 5)Serial.print(":");
}

To finalize the handling function, we will print the received data. Recall from the sender that we have sent an integer. So, we need to cast the received data to this data type.

Note however that we receive as input of the handling function a pointer to the data. So, we first use the * operator (deference operator) to access the actual value, and then cast it to int.

Serial.println((int)*data);

The complete callback function can be seen below.

void onReceiveData(const uint8_t *mac, const uint8_t *data, int len) {

  Serial.print("Received from MAC: ");

  for (int i = 0; i < 6; i++) {

    Serial.printf("%02X", mac[i]);
    if (i < 5)Serial.print(":");
  }

  Serial.println();
  Serial.println((int)*data);

}

The final code can be seen below.

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

void onReceiveData(const uint8_t *mac, const uint8_t *data, int len) {

  Serial.print("Received from MAC: ");

  for (int i = 0; i < 6; i++) {

    Serial.printf("%02X", mac[i]);
    if (i < 5)Serial.print(":");
  }

  Serial.println();
  Serial.println((int)*data);

}

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);

  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  esp_now_register_recv_cb(onReceiveData);
}

void loop() {}

Testing the code

To test the system end to end, we will need to have two instances of the Arduino IDE opened, so we can check what each of the devices (sender and receiver) are printing to the serial monitor.

First, compile and upload the code of the receiver. We should have the receiver running first because the sender will send the data only once. After the code is uploaded, open the Arduino IDE serial monitor.

When the receiver is running, compile and upload the sender’s code. After the procedure finishes, open also the serial monitor on that instance of the Arduino IDE.

You should get an output similar to figure 1. As can be seen, the MAC address of the sender device should get printed. After that line, you should get a message indicating the ESP-NOW message was sent with success.

Output of the sender on the Arduino IDE serial monitor.
Figure 1 – Output of the sender.

Then go back to the serial monitor connected to the receiver. You should get a result similar to figure 2.

As can be seen, the MAC printed matches the one from the sender and we also obtained the payload sent.

Output of the receiver on the Arduino IDE serial monitor.
Figure 2 – Output of the receiver.

References

[1] https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/network/esp_now.html

Leave a Reply