ESP-NOW: Auto-pairing for ESP32/ESP8266 with Bidirectional Communication and Web Server

This guide shows how to build an ESP32 web server and use ESP-NOW communication protocol simultaneously. We’ll show you how to establish a two-way communication between the master (web server) and slaves, and how to automatically add boards to the network (auto-pairing).

ESP-NOW Auto-pairing for ESP32 ESP8266 with Bidirectional Communication and Web Server

This tutorial is an improvement of the following:

The new version includes:

  • Two-way communication between the server and the slaves;
  • Auto-pairing peers—you don’t need to know any of the boards’ MAC addresses. You don’t need to add peers manually. You just need to run the codes provided and the boards will be automatically added to the ESP-NOW network.

The improvements were suggested by one of our readers (Jean-Claude Servaye). You can find the original codes on his GitHub page.

If you’re new to ESP-NOW, we recommend getting familiar with ESP-NOW concepts and functions first. Check the following getting started guides:

Using ESP-NOW and Wi-Fi (Web Server) Simultaneously

There are a few things you need to take into account if you want to use Wi-Fi to host a web server and use ESP-NOW simultaneously to receive sensor readings from other boards:

ESP-NOW and Wi-Fi simultaneously. The web server and the sender boards must be on the same wi-fi channel.
  • The ESP32/ESP8266 sender boards must use the same Wi-Fi channel as the receiver board (server).
  • The Wi-Fi channel of the receiver board is automatically assigned by your Wi-Fi router.
  • The Wi-Fi mode of the receiver board must be access point and station (WIFI_AP_STA).
  • You can set up the same Wi-Fi channel manually, but we’ll do it automatically. The sender will try different Wi-Fi channels until it gets a response from the server.

Project Overview

Here’s a quick overview of the example we’ll build:

ESP-NOW Web Server Example
  • There are two ESP sender boards (ESP32 or ESP8266) that send readings* via ESP-NOW to one ESP32 receiver board (ESP-NOW many to one configuration);
  • The receiver board receives the packets and displays the readings on a web page;
  • The web page is updated automatically every time it receives a new reading using Server-Sent Events (SSE);
  • The receiver also sends data to the sender—this is to illustrate how to establish bidirectional communication. As an example, we’ll send arbitrary values, but you can easily replace them with sensor readings or any other data like threshold values, or commands to turn on/off GPIOs.

*we’ll send arbitrary temperature and humidity values—we won’t use an actual sensor. After testing the project and checking that everything is working as expected you can use a sensor of your choice (it doesn’t have to be temperature or humidity).

Auto-Pairing

Here’s how the auto-pairing with peers (sender(server)/slave boards) works:

ESP-NOW Auto pairing diagram
  • The peer sends a message of type PAIRING to the server (1) using the broadcast MAC address ff:ff:ff:ff:ff:ff. When you send data to this MAC address, all ESP-NOW devices receive the message. For the server to receive the message, they need to communicate on the same Wi-Fi channel.
  • If the peer doesn’t receive a message from the server, it tries to send the same message on a different Wi-Fi channel. It repeats the process until it gets a message from the server.
  • The server receives the message and the address of the peer (2).
  • The server adds the address of the peer to his peer list (3).
  • The server replies to the peer with a message of type PAIRING with its information (MAC address and channel) (4).
  • The peer receives the message and the WiFi.macAddress of the server (5).
  • The peer adds the received address of the server to his peer list (6).
  • The peer tries to send a message to the server address but it fails to transmit*.
    1. The peer adds the WiFi.softAPmacAddress of the server to his peer list.
    2. The peer sends a message to the server WiFi.softAPmacAddress.
  • The server receives the message from the peer.
  • They can now communicate bidirectionally (6).

*ESP32 in WIFI_AP_STA mode responds with its WiFi.macAddress but it uses WiFi.softAPmacAddress to receive from ESP8266 peer.

WiFi.softAPmacAddress is created from WiFi.macAddress by adding 1 to the last byte—check the documentation.

Prerequisites

Before proceeding with this project, make sure you check the following prerequisites.

Arduino IDE

We’ll program the ESP32 and ESP8266 boards using Arduino IDE, so before proceeding with this tutorial, make sure you have the ESP32 and ESP8266 boards installed in your Arduino IDE.

Async Web Server Libraries

To build the web server you need to install the following libraries:

These libraries aren’t available to install through the Arduino Library Manager, so you need to copy the library files to the Arduino Installation Libraries folder. Alternatively, in your Arduino IDE, you can go to Sketch Include Library > Add .zip Library and select the libraries you’ve just downloaded.

Arduino_JSON Library

Our examples will use the ArduinoJSON library by Benoit Blanchon version 7.0.4. You can install this library in the Arduino IDE Library Manager. Just go to Sketch Include Library > Manage Libraries and search for the library name ArduinoJSON as follows:

Install ArduinoJson library arduino ide 2

Parts Required

To test this project, you need at least three ESP boards. One ESP32 board to act as a server and two sender/slave ESP boards that can be ESP32 or ESP8266.

ESP32 Server

Here are the server features:

  • Pairs automatically with peers (other ESP-NOW boards);
  • Receives packets from peers;
  • Hosts a web server to display the latest received packets;
  • Also sends data back to the other boards (bidirectional communication with peers).

ESP32 Server Code

Upload the following code to your ESP32 board. This can receive data from multiple boards. However, the web page is just prepared to display data from two boards. You can easily modify the web page to accommodate more boards.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp-now-auto-pairing-esp32-esp8266/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  Based on JC Servaye example: https://github.com/Servayejc/esp_now_web_server/
*/
#include <esp_now.h>
#include <WiFi.h>
#include <esp_wifi.h>
#include "ESPAsyncWebServer.h"
#include "AsyncTCP.h"
#include <ArduinoJson.h>

// Replace with your network credentials (STATION)
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

esp_now_peer_info_t slave;
int chan; 

enum MessageType {PAIRING, DATA,};
MessageType messageType;

int counter = 0;

uint8_t clientMacAddress[6];

// Structure example to receive data
// Must match the sender structure
typedef struct struct_message {
  uint8_t msgType;
  uint8_t id;
  float temp;
  float hum;
  unsigned int readingId;
} struct_message;

typedef struct struct_pairing {       // new structure for pairing
    uint8_t msgType;
    uint8_t id;
    uint8_t macAddr[6];
    uint8_t channel;
} struct_pairing;

struct_message incomingReadings;
struct_message outgoingSetpoints;
struct_pairing pairingData;

AsyncWebServer server(80);
AsyncEventSource events("/events");

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP-NOW DASHBOARD</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <link rel="icon" href="data:,">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    p {  font-size: 1.2rem;}
    body {  margin: 0;}
    .topnav { overflow: hidden; background-color: #2f4468; color: white; font-size: 1.7rem; }
    .content { padding: 20px; }
    .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }
    .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
    .reading { font-size: 2.8rem; }
    .packet { color: #bebebe; }
    .card.temperature { color: #fd7e14; }
    .card.humidity { color: #1b78e2; }
  </style>
</head>
<body>
  <div class="topnav">
    <h3>ESP-NOW DASHBOARD</h3>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card temperature">
        <h4><i class="fas fa-thermometer-half"></i> BOARD #1 - TEMPERATURE</h4><p><span class="reading"><span id="t1"></span> &deg;C</span></p><p class="packet">Reading ID: <span id="rt1"></span></p>
      </div>
      <div class="card humidity">
        <h4><i class="fas fa-tint"></i> BOARD #1 - HUMIDITY</h4><p><span class="reading"><span id="h1"></span> &percnt;</span></p><p class="packet">Reading ID: <span id="rh1"></span></p>
      </div>
      <div class="card temperature">
        <h4><i class="fas fa-thermometer-half"></i> BOARD #2 - TEMPERATURE</h4><p><span class="reading"><span id="t2"></span> &deg;C</span></p><p class="packet">Reading ID: <span id="rt2"></span></p>
      </div>
      <div class="card humidity">
        <h4><i class="fas fa-tint"></i> BOARD #2 - HUMIDITY</h4><p><span class="reading"><span id="h2"></span> &percnt;</span></p><p class="packet">Reading ID: <span id="rh2"></span></p>
      </div>
    </div>
  </div>
<script>
if (!!window.EventSource) {
 var source = new EventSource('/events');
 
 source.addEventListener('open', function(e) {
  console.log("Events Connected");
 }, false);
 source.addEventListener('error', function(e) {
  if (e.target.readyState != EventSource.OPEN) {
    console.log("Events Disconnected");
  }
 }, false);
 
 source.addEventListener('message', function(e) {
  console.log("message", e.data);
 }, false);
 
 source.addEventListener('new_readings', function(e) {
  console.log("new_readings", e.data);
  var obj = JSON.parse(e.data);
  document.getElementById("t"+obj.id).innerHTML = obj.temperature.toFixed(2);
  document.getElementById("h"+obj.id).innerHTML = obj.humidity.toFixed(2);
  document.getElementById("rt"+obj.id).innerHTML = obj.readingId;
  document.getElementById("rh"+obj.id).innerHTML = obj.readingId;
 }, false);
}
</script>
</body>
</html>)rawliteral";

void readMacAddress(){
  uint8_t baseMac[6];
  esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, baseMac);
  if (ret == ESP_OK) {
    Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x\n",
                  baseMac[0], baseMac[1], baseMac[2],
                  baseMac[3], baseMac[4], baseMac[5]);
  } else {
    Serial.println("Failed to read MAC address");
  }
}

void readDataToSend() {
  outgoingSetpoints.msgType = DATA;
  outgoingSetpoints.id = 0;
  outgoingSetpoints.temp = random(0, 40);
  outgoingSetpoints.hum = random(0, 100);
  outgoingSetpoints.readingId = counter++;
}

// ---------------------------- esp_ now -------------------------
void printMAC(const uint8_t * mac_addr){
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print(macStr);
}

bool addPeer(const uint8_t *peer_addr) {      // add pairing
  memset(&slave, 0, sizeof(slave));
  const esp_now_peer_info_t *peer = &slave;
  memcpy(slave.peer_addr, peer_addr, 6);
  
  slave.channel = chan; // pick a channel
  slave.encrypt = 0; // no encryption
  // check if the peer exists
  bool exists = esp_now_is_peer_exist(slave.peer_addr);
  if (exists) {
    // Slave already paired.
    Serial.println("Already Paired");
    return true;
  }
  else {
    esp_err_t addStatus = esp_now_add_peer(peer);
    if (addStatus == ESP_OK) {
      // Pair success
      Serial.println("Pair success");
      return true;
    }
    else 
    {
      Serial.println("Pair failed");
      return false;
    }
  }
} 

// callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("Last Packet Send Status: ");
  Serial.print(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success to " : "Delivery Fail to ");
  printMAC(mac_addr);
  Serial.println();
}

void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) { 
  Serial.print(len);
  Serial.println(" bytes of new data received.");
  StaticJsonDocument<1000> root;
  String payload;
  uint8_t type = incomingData[0];       // first message byte is the type of message 
  switch (type) {
  case DATA :                           // the message is data type
    memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));
    // create a JSON document with received data and send it by event to the web page
    root["id"] = incomingReadings.id;
    root["temperature"] = incomingReadings.temp;
    root["humidity"] = incomingReadings.hum;
    root["readingId"] = String(incomingReadings.readingId);
    serializeJson(root, payload);
    Serial.print("event send :");
    serializeJson(root, Serial);
    events.send(payload.c_str(), "new_readings", millis());
    Serial.println();
    break;
  
  case PAIRING:                            // the message is a pairing request 
    memcpy(&pairingData, incomingData, sizeof(pairingData));
    Serial.println(pairingData.msgType);
    Serial.println(pairingData.id);
    Serial.print("Pairing request from MAC Address: ");
    printMAC(pairingData.macAddr);
    Serial.print(" on channel ");
    Serial.println(pairingData.channel);

    clientMacAddress[0] = pairingData.macAddr[0];
    clientMacAddress[1] = pairingData.macAddr[1];
    clientMacAddress[2] = pairingData.macAddr[2];
    clientMacAddress[3] = pairingData.macAddr[3];
    clientMacAddress[4] = pairingData.macAddr[4];
    clientMacAddress[5] = pairingData.macAddr[5];

    if (pairingData.id > 0) {     // do not replay to server itself
      if (pairingData.msgType == PAIRING) { 
        pairingData.id = 0;       // 0 is server
        // Server is in AP_STA mode: peers need to send data to server soft AP MAC address 
        WiFi.softAPmacAddress(pairingData.macAddr);
        Serial.print("Pairing MAC Address: ");
        printMAC(clientMacAddress);
        pairingData.channel = chan;
        Serial.println(" send response");
        esp_err_t result = esp_now_send(clientMacAddress, (uint8_t *) &pairingData, sizeof(pairingData));
        addPeer(clientMacAddress);
      }  
    }  
    break; 
  }
}

void initESP_NOW(){
    // Init ESP-NOW
    if (esp_now_init() != ESP_OK) {
      Serial.println("Error initializing ESP-NOW");
      return;
    }
    esp_now_register_send_cb(OnDataSent);
    esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
} 

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.STA.begin();
  Serial.print("Server MAC Address: ");
  readMacAddress();

  // Set the device as a Station and Soft Access Point simultaneously
  WiFi.mode(WIFI_AP_STA);
  // Set device as a Wi-Fi Station
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Setting as a Wi-Fi Station..");
  }

  Serial.print("Server SOFT AP MAC Address:  ");
  Serial.println(WiFi.softAPmacAddress());

  chan = WiFi.channel();
  Serial.print("Station IP Address: ");
  Serial.println(WiFi.localIP());
  Serial.print("Wi-Fi Channel: ");
  Serial.println(WiFi.channel());

  initESP_NOW();
  
  // Start Web server
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });
  
  // Events 
  events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis
    // and set reconnect delay to 1 second
    client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&events);
  // start server
  server.begin();
}

void loop() {
  static unsigned long lastEventTime = millis();
  static const unsigned long EVENT_INTERVAL_MS = 5000;
  if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
    events.send("ping", NULL, millis());
    lastEventTime = millis();
    readDataToSend();
    esp_now_send(NULL, (uint8_t *) &outgoingSetpoints, sizeof(outgoingSetpoints));
  }
}

View raw code

How the Code Works

We already explained how the server code works in great detail in a previous project. So, we’ll just take a look at the relevant parts for auto-pairing.

Message Types

The server and senders can exchange two types of messages: messages with pairing data with MAC address, channel, and board id, and messages with the actual data like sensor readings.

So, we create an enumerated type that holds the possible incoming message types (PAIRING and DATA).

enum MessageType {PAIRING, DATA,};

An enumerated type is a data type (usually user-defined) consisting of a set of named constants called enumerators. The act of creating an enumerated type defines an enumeration. When an identifier such as a variable is declared having an enumerated type, the variable can be assigned any of the enumerators as a value“. Source: https://playground.arduino.cc/Code/Enum/

After that, we create a variable of that type we’ve just created called messageType. Remember that this variable can only have two possible values: PAIRING or DATA.

MessageType messageType;

Data Structure

Create a structure that will contain the data we’ll receive. We called this structure struct_message and it contains the message type (so that we know if we received a message with data or with peer info), board ID, temperature and humidity readings, and the reading ID.

typedef struct struct_message {
  uint8_t msgType;
  uint8_t id;
  float temp;
  float hum;
  unsigned int readingId;
} struct_message;

We also need another structure to contain the peer information for pairing the peer. We call this structure struct_pairing. This structure will contain the message type, board id, mac address of the sender board, and Wi-Fi channel.

typedef struct struct_pairing {       // new structure for pairing
    uint8_t msgType;
    uint8_t id;
    uint8_t macAddr[6];
    uint8_t channel;
} struct_pairing;

We create two variables of type struct_message, one called incomingReadings that will store the readings coming from the slaves, and another called outgoingSetpoints that will hold the data to send to the slaves.

struct_message incomingReadings;
struct_message outgoingSetpoints;

We also create a variable of type struct_pairing to hold the peer information.

struct_pairing pairingData;

readDataToSend() Function

The readDataToSend() should be used to get data from whichever sensor you’re using and put them on the associated structure to be sent to the slave boards.

void readDataToSend() {
  outgoingSetpoints.msgType = DATA;
  outgoingSetpoints.id = 0;
  outgoingSetpoints.temp = random(0, 40);
  outgoingSetpoints.hum = random(0, 100);
  outgoingSetpoints.readingId = counter++;
}

The msgType should be DATA. The id corresponds to the board id (we’re setting the server board ID to 0, the others boards should have id=1, 2, 3, and so on). Finally, temp and hum hold the sensor readings. In this case, we’re setting them to random values. You should replace that with the correct functions to get data from your sensor. Every time we send a new set of readings, we increase the counter variable.

Adding a Peer

We create a function called addPeer() that will return a boolean variable (either true or false) that indicates whether the pairing process was successful or not. This function tries to add peers. It will be called later when the board receives a message of type PAIRING. If the peer is already on the list of peers, it returns true. It also returns true if the peer is successfully added. It returns false, if it fails to add the peer to the list.

bool addPeer(const uint8_t *peer_addr) {      // add pairing
  memset(&slave, 0, sizeof(slave));
  const esp_now_peer_info_t *peer = &slave;
  memcpy(slave.peer_addr, peer_addr, 6);
  
  slave.channel = chan; // pick a channel
  slave.encrypt = 0; // no encryption
  // check if the peer exists
  bool exists = esp_now_is_peer_exist(slave.peer_addr);
  if (exists) {
    // Slave already paired.
    Serial.println("Already Paired");
    return true;
  }
  else {
    esp_err_t addStatus = esp_now_add_peer(peer);
    if (addStatus == ESP_OK) {
      // Pair success
      Serial.println("Pair success");
      return true;
    }
    else 
    {
      Serial.println("Pair failed");
      return false;
    }
  }
} 

Receiving and Handling ESP-NOW Messages

The OnDataRecv() function will be executed when you receive a new ESP-NOW packet.

void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {

Inside that function, print the length of the message.

Serial.print(len);
Serial.print(" bytes of data received from : ");

Previously, we’ve seen that we can receive two types of messages: PAIRING and DATA. So, we must handle the message content differently depending on the type of message. We can get the type of message as follows:

uint8_t type = incomingData[0];       // first message byte is the type of message

Then, we’ll run different codes depending if the message is of type DATA or PAIRING.

If it is of type DATA, copy the information in the incomingData variable into the incomingReadings structure variable.

memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));

Then, create a JSON document with the received information (root):

// create a JSON document with received data and send it by event to the web page
root["id"] = incomingReadings.id;
root["temperature"] = incomingReadings.temp;
root["humidity"] = incomingReadings.hum;
root["readingId"] = String(incomingReadings.readingId);

Convert the JSON document to a string (payload):

serializeJson(root, payload);

After gathering all the received data on the payload variable, send that information to the browser as an event (“new_readings”).

events.send(payload.c_str(), "new_readings", millis());

We’ve seen on a previous project how to handle these events on the client side.

If the message is of type PAIRING, it contains the peer information.

case PAIRING:                            // the message is a pairing request

We save the received data in the incomingData variable and print the details on the Serial Monitor.

memcpy(&pairingData, incomingData, sizeof(pairingData));
Serial.println(pairingData.msgType);
Serial.println(pairingData.id);
Serial.print("Pairing request from MAC Address: ");
printMAC(pairingData.macAddr);
Serial.print(" on channel ");
Serial.println(pairingData.channel);

The server responds back with its MAC address (in access point mode) and channel, so that the peer knows it sent the information using the right channel and can add the server as peer.

if (pairingData.id > 0) {     // do not replay to server itself
  if (pairingData.msgType == PAIRING) { 
    pairingData.id = 0;       // 0 is server
    // Server is in AP_STA mode: peers need to send data to server soft AP MAC address 
    WiFi.softAPmacAddress(pairingData.macAddr);
    Serial.print("Pairing MAC Address: ");
    printMAC(clientMacAddress);
    pairingData.channel = chan;
    Serial.println(" send response");
    esp_err_t result = esp_now_send(clientMacAddress, (uint8_t *) &pairingData, sizeof(pairingData));

Finally, the server adds the sender to its peer list using the addPeer() function we created previously.

addPeer(clientMacAddress);

Initialize ESP-NOW

The initESP_NOW() function intializes ESP-NOW and registers the callback functions for when data is sent and received.

void initESP_NOW(){
    // Init ESP-NOW
    if (esp_now_init() != ESP_OK) {
      Serial.println("Error initializing ESP-NOW");
      return;
    }
    esp_now_register_send_cb(OnDataSent);
    esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
}

setup()

In the setup(), print the board MAC address:

readMacAddress();

Set the ESP32 receiver as station and soft access point simultaneously:

WiFi.mode(WIFI_AP_STA);

The following lines connect the ESP32 to your local network and print the IP address and the Wi-Fi channel:

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Setting as a Wi-Fi Station..");
}

Print the board MAC address in access point mode, which is different than the MAC address on station mode.

Serial.print("Server SOFT AP MAC Address:  ");
Serial.println(WiFi.softAPmacAddress());

Get the board Wi-Fi channel and print it in the Serial Monitor.

chan = WiFi.channel();
Serial.print("Station IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("Wi-Fi Channel: ");
Serial.println(WiFi.channel());

Initialize ESP-NOW by calling the initESP_NOW() function we created previously.

initESP_NOW();

Send Data Messages to the Sender Boards

In the loop(), every 5 seconds (EVENT_INTERVAL_MS) get data from a sensor or sample data by calling the readDataToSend() function. It adds new data to the outgoingSetpoints structure.

readDataToSend();

Finally, send that data to all registered peers.

esp_now_send(NULL, (uint8_t *) &outgoingSetpoints, sizeof(outgoingSetpoints));

That’s pretty much how the server code works when it comes to handling ESP-NOW messages and automatically adding peers.

Testing the Server

After uploading the code to the receiver board, press the on-board EN/RST button. The ESP32 IP address should be printed on the Serial Monitor as well as the Wi-Fi channel.

ESP32 Server ESP-NOW Protocol Autopairing Arduino IDE Serial Demonstration

You can access the web server on the board’s IP address. At the moment, there won’t be any data displayed because we haven’t prepared the sender boards yet. Let the server board run the code.

ESP32/ESP8266 Sender

Here are the sender board features:

  • Pairs automatically with server;
  • Sends packets with sensor readings to server;
  • Also receives data from the server (bidirectional communication).

Auto-Pairing

Here’s how the auto-pairing with the server works:

  • The sender doesn’t have access to the router;
  • The sender doesn’t know the server’s MAC address;
  • The server must be running for this to work (with the previous code);
  • The sender sets esp now on channel 1;
  • The server adds an entry with the broadcast address to its peer list;
  • The sender sends a PAIRING message request in broadcast mode:
    • If the server receives the message we are on the correct channel:
    • The server adds the received MAC to his peer list (previous section);
    • The server replies to the MAC address with a message containing his channel number and MAC address (previous section);
    • The sender replaces the broadcast address with the server address in his peer list.
  • else
    • The sender repeats the process on the next channel.

WiFi.softAPmacAddress is created from WiFi.macAddress by adding 1 to the last byte—check the documentation.

ESP32 Sender Code

Upload the following code to your ESP32 board.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp-now-auto-pairing-esp32-esp8266/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 
  Based on JC Servaye example: https://github.com/Servayejc/esp_now_sender/
*/
#include <Arduino.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#include <EEPROM.h>

// Set your Board and Server ID 
#define BOARD_ID 1
#define MAX_CHANNEL 13  // 11 in North America or 13 in Europe

uint8_t serverAddress[] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
uint8_t clientMacAddress[6];

// Structure to send data
// Must match the receiver structure
// Structure example to receive data
// Must match the sender structure
typedef struct struct_message {
  uint8_t msgType;
  uint8_t id;
  float temp;
  float hum;
  unsigned int readingId;
} struct_message;

typedef struct struct_pairing {       // new structure for pairing
    uint8_t msgType;
    uint8_t id;
    uint8_t macAddr[6];
    uint8_t channel;
} struct_pairing;

esp_now_peer_info_t peer;

// Create 2 struct_message 
struct_message myData;  // data to send
struct_message inData;  // data received
struct_pairing pairingData;

enum PairingStatus {NOT_PAIRED, PAIR_REQUEST, PAIR_REQUESTED, PAIR_PAIRED,};
PairingStatus pairingStatus = NOT_PAIRED;

enum MessageType {PAIRING, DATA,};
MessageType messageType;

#ifdef SAVE_CHANNEL
  int lastChannel;
#endif  
int channel = 1;
 
// simulate temperature and humidity data
float t = 0;
float h = 0;

unsigned long currentMillis = millis();
unsigned long previousMillis = 0;   // Stores last time temperature was published
const long interval = 10000;        // Interval at which to publish sensor readings
unsigned long start;                // used to measure Pairing time
unsigned int readingId = 0;   

void readGetMacAddress(){
  uint8_t baseMac[6];
  esp_err_t ret = esp_wifi_get_mac(WIFI_IF_STA, baseMac);
  if (ret == ESP_OK) {
    Serial.printf("%02x:%02x:%02x:%02x:%02x:%02x\n",
                  baseMac[0], baseMac[1], baseMac[2],
                  baseMac[3], baseMac[4], baseMac[5]);
  } else {
    Serial.println("Failed to read MAC address");
  }
  clientMacAddress[0] = baseMac[0];
  clientMacAddress[1] = baseMac[1];
  clientMacAddress[2] = baseMac[2];
  clientMacAddress[3] = baseMac[3];
  clientMacAddress[4] = baseMac[4];
  clientMacAddress[5] = baseMac[5];
}

// simulate temperature reading
float readDHTTemperature() {
  t = random(0,40);
  return t;
}

// simulate humidity reading
float readDHTHumidity() {
  h = random(0,100);
  return h;
}

void addPeer(const uint8_t * mac_addr, uint8_t chan){
  ESP_ERROR_CHECK(esp_wifi_set_channel(chan ,WIFI_SECOND_CHAN_NONE));
  esp_now_del_peer(mac_addr);
  memset(&peer, 0, sizeof(esp_now_peer_info_t));
  peer.channel = chan;
  peer.encrypt = false;
  memcpy(peer.peer_addr, mac_addr, sizeof(uint8_t[6]));
  if (esp_now_add_peer(&peer) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  }
  memcpy(serverAddress, mac_addr, sizeof(uint8_t[6]));
}

void printMAC(const uint8_t * mac_addr){
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print(macStr);
}

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 OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) { 
  Serial.print("Packet received with ");
  Serial.print("data size = ");
  Serial.println(sizeof(incomingData));
  uint8_t type = incomingData[0];
  switch (type) {
  case DATA :      // we received data from server
    memcpy(&inData, incomingData, sizeof(inData));
    Serial.print("ID  = ");
    Serial.println(inData.id);
    Serial.print("Setpoint temp = ");
    Serial.println(inData.temp);
    Serial.print("SetPoint humidity = ");
    Serial.println(inData.hum);
    Serial.print("reading Id  = ");
    Serial.println(inData.readingId);

    if (inData.readingId % 2 == 1){
      digitalWrite(LED_BUILTIN, LOW);
    } else { 
      digitalWrite(LED_BUILTIN, HIGH);
    }
    break;

  case PAIRING:    // we received pairing data from server
    memcpy(&pairingData, incomingData, sizeof(pairingData));
    if (pairingData.id == 0) {              // the message comes from server
      Serial.print("Pairing done for MAC Address: ");
      printMAC(pairingData.macAddr);
      Serial.print(" on channel " );
      Serial.print(pairingData.channel);    // channel used by the server
      Serial.print(" in ");
      Serial.print(millis()-start);
      Serial.println("ms");
      addPeer(pairingData.macAddr, pairingData.channel); // add the server  to the peer list 
      #ifdef SAVE_CHANNEL
        lastChannel = pairingData.channel;
        EEPROM.write(0, pairingData.channel);
        EEPROM.commit();
      #endif  
      pairingStatus = PAIR_PAIRED;             // set the pairing status
    }
    break;
  }  
}

PairingStatus autoPairing(){
  switch(pairingStatus) {
    case PAIR_REQUEST:
      Serial.print("Pairing request on channel "  );
      Serial.println(channel);

      // set WiFi channel   
      ESP_ERROR_CHECK(esp_wifi_set_channel(channel,  WIFI_SECOND_CHAN_NONE));
      if (esp_now_init() != ESP_OK) {
        Serial.println("Error initializing ESP-NOW");
      }

      // set callback routines
      esp_now_register_send_cb(OnDataSent);
      esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
    
      // set pairing data to send to the server
      pairingData.msgType = PAIRING;
      pairingData.id = BOARD_ID;     
      pairingData.channel = channel;
      pairingData.macAddr[0] = clientMacAddress[0];
      pairingData.macAddr[1] = clientMacAddress[1];
      pairingData.macAddr[2] = clientMacAddress[2];
      pairingData.macAddr[3] = clientMacAddress[3];
      pairingData.macAddr[4] = clientMacAddress[4];
      pairingData.macAddr[5] = clientMacAddress[5];

      // add peer and send request
      addPeer(serverAddress, channel);
      esp_now_send(serverAddress, (uint8_t *) &pairingData, sizeof(pairingData));
      previousMillis = millis();
      pairingStatus = PAIR_REQUESTED;
      break;

    case PAIR_REQUESTED:
      // time out to allow receiving response from server
      currentMillis = millis();
      if(currentMillis - previousMillis > 1000) {
        previousMillis = currentMillis;
        // time out expired,  try next channel
        channel ++;
        if (channel > MAX_CHANNEL){
          channel = 1;
        }   
        pairingStatus = PAIR_REQUEST;
      }
    break;

    case PAIR_PAIRED:
      // nothing to do here 
    break;
  }
  return pairingStatus;
}  

void setup() {
  Serial.begin(115200);
  Serial.println();
  pinMode(LED_BUILTIN, OUTPUT);
  
  WiFi.mode(WIFI_STA);
  WiFi.STA.begin();
  Serial.print("Client Board MAC Address:  ");
  readGetMacAddress();
  WiFi.disconnect();
  start = millis();

  #ifdef SAVE_CHANNEL 
    EEPROM.begin(10);
    lastChannel = EEPROM.read(0);
    Serial.println(lastChannel);
    if (lastChannel >= 1 && lastChannel <= MAX_CHANNEL) {
      channel = lastChannel; 
    }
    Serial.println(channel);
  #endif  
  pairingStatus = PAIR_REQUEST;
}  

void loop() {
  if (autoPairing() == PAIR_PAIRED) {
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= interval) {
      // Save the last time a new reading was published
      previousMillis = currentMillis;
      //Set values to send
      myData.msgType = DATA;
      myData.id = BOARD_ID;
      myData.temp = readDHTTemperature();
      myData.hum = readDHTHumidity();
      myData.readingId = readingId++;
      esp_err_t result = esp_now_send(serverAddress, (uint8_t *) &myData, sizeof(myData));
    }
  }
}

View raw code

ESP8266 Sender Code

If you’re using ESP8266 boards, use the following code instead. It’s similar to the previous code but uses the ESP8266-specific ESP-NOW functions.

/*
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp-now-auto-pairing-esp32-esp8266/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  Based on JC Servaye example: https://https://github.com/Servayejc/esp8266_espnow
*/
#include <ESP8266WiFi.h>
#include <espnow.h>

uint8_t channel = 1;
int readingId = 0;
int id = 2;

#define MAX_CHANNEL 13  // 11 in North America or 13 in Europe

unsigned long currentMillis = millis(); 
unsigned long lastTime = 0;  
unsigned long timerDelay = 2000;  // send readings timer

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

uint8_t clientMacAddress[6];

enum PairingStatus {PAIR_REQUEST, PAIR_REQUESTED, PAIR_PAIRED, };
PairingStatus pairingStatus = PAIR_REQUEST;

enum MessageType {PAIRING, DATA,};
MessageType messageType;

// Define variables to store DHT readings to be sent
float temperature;
float humidity;

// Define variables to store incoming readings
float incomingTemp;
float incomingHum;
int incomingReadingsId;

// Updates DHT readings every 10 seconds
//const long interval = 10000; 
unsigned long previousMillis = 0;    // will store last time DHT was updated 

//Structure example to send data
//Must match the receiver structure
typedef struct struct_message {
  uint8_t msgType;
  uint8_t id;
  float temp;
  float hum;
  unsigned int readingId;
} struct_message;

typedef struct struct_pairing {       // new structure for pairing
    uint8_t msgType;
    uint8_t id;
    uint8_t macAddr[6];
    uint8_t channel;
} struct_pairing;

// Create a struct_message called myData
struct_message myData;
struct_message incomingReadings;
struct_pairing pairingData;

#define BOARD_ID 2
unsigned long start;

// Callback when data is sent
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus) {
  Serial.print("Last Packet Send Status: ");
  if (sendStatus == 0){
    Serial.println("Delivery success");
  }
  else{
    Serial.println("Delivery fail");
  }
}

void printMAC(const uint8_t * mac_addr){
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print(macStr);
}

void printIncomingReadings(){
  // Display Readings in Serial Monitor
  Serial.println("INCOMING READINGS");
  Serial.print("Temperature: ");
  Serial.print(incomingTemp);
  Serial.println(" ºC");
  Serial.print("Humidity: ");
  Serial.print(incomingHum);
  Serial.println(" %");
  Serial.print("Led: ");
  Serial.print(incomingReadingsId);
}

// Callback when data is received
void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) {
  Serial.print("Size of message : ");
  Serial.print(len);
  Serial.print(" from ");
  printMAC(mac);
  Serial.println();
  uint8_t type = incomingData[0];
  switch (type) {
  case DATA :  
    memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));
    Serial.print(len);
    Serial.print(" Data bytes received from: ");
    printMAC(mac);
    Serial.println();
    incomingTemp = incomingReadings.temp;
    incomingHum = incomingReadings.hum;
    printIncomingReadings();
    
    if (incomingReadings.readingId % 2 == 1){
      digitalWrite(LED_BUILTIN, LOW);
    } else { 
      digitalWrite(LED_BUILTIN, HIGH);
    }
    break;

  case PAIRING:
    memcpy(&pairingData, incomingData, sizeof(pairingData));
    if (pairingData.id == 0) {                // the message comes from server
      Serial.print("Pairing done for ");
      printMAC(pairingData.macAddr);
      Serial.print(" on channel " );
      Serial.print(pairingData.channel);    // channel used by the server
      Serial.print(" in ");
      Serial.print(millis()-start);
      Serial.println("ms");
      //esp_now_del_peer(pairingData.macAddr);
      //esp_now_del_peer(mac);
      esp_now_add_peer(pairingData.macAddr, ESP_NOW_ROLE_COMBO, pairingData.channel, NULL, 0); // add the server to the peer list 
      pairingStatus = PAIR_PAIRED ;            // set the pairing status
    }
    break;
  }  
}

void getReadings(){
  // Read Temperature
  temperature = 22.5;
  humidity = 55.5;
}

void readGetMacAddress(){
  String val = WiFi.macAddress();
  Serial.println(val);
  char* endPtr; 
  clientMacAddress[0] = strtol(val.c_str(), &endPtr, 16); // read the first starting at the beginning of the buffer. this initializes endPtr as a pointer to the ':' after the first number 
  for (int i = 1;  (*endPtr) && (i < 6); i++) {
    clientMacAddress[i] = strtol(endPtr + 1, &endPtr, 16); // using +1 for the pointer as we want to skip the ':'
  }

  for (int i = 0; i < 6; i++) {
    Serial.print(clientMacAddress[i], HEX);
    if (i != 5) Serial.print(F(":"));
  }
}

PairingStatus autoPairing(){
  switch(pairingStatus) {
  case PAIR_REQUEST:
    Serial.print("Pairing request on channel "  );
    Serial.println(channel);
  
    // clean esp now
    esp_now_deinit();
    WiFi.mode(WIFI_STA);
    // set WiFi channel   
    wifi_promiscuous_enable(1);
    wifi_set_channel(channel);
    wifi_promiscuous_enable(0);
    //WiFi.printDiag(Serial);
    WiFi.disconnect();

    // Init ESP-NOW
    if (esp_now_init() != 0) {
      Serial.println("Error initializing ESP-NOW");
    }
    esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
    // set callback routines
    esp_now_register_send_cb(OnDataSent);
    esp_now_register_recv_cb(OnDataRecv);
    
    // set pairing data to send to the server
    pairingData.id = BOARD_ID;     
    pairingData.channel = channel;
    pairingData.macAddr[0] = clientMacAddress[0];
    pairingData.macAddr[1] = clientMacAddress[1];
    pairingData.macAddr[2] = clientMacAddress[2];
    pairingData.macAddr[3] = clientMacAddress[3];
    pairingData.macAddr[4] = clientMacAddress[4];
    pairingData.macAddr[5] = clientMacAddress[5];
    previousMillis = millis();
    // add peer and send request
    Serial.println(esp_now_send(broadcastAddressX, (uint8_t *) &pairingData, sizeof(pairingData)));
    pairingStatus = PAIR_REQUESTED;
    break;

  case PAIR_REQUESTED:
    // time out to allow receiving response from server
    currentMillis = millis();
    if(currentMillis - previousMillis > 1000) {
      previousMillis = currentMillis;
      // time out expired,  try next channel
      channel ++;
      if (channel > MAX_CHANNEL) {
        channel = 0;
      }
      pairingStatus = PAIR_REQUEST; 
    }
    break;

  case PAIR_PAIRED:
    //Serial.println("Paired!");
    break;
  }
  return pairingStatus;
} 



void setup() {
  // Init Serial Monitor
  Serial.begin(74880);
  pinMode(LED_BUILTIN, OUTPUT);
  // Init DHT sensor
 
  // Set device as a Wi-Fi Station
  WiFi.mode(WIFI_STA);
  readGetMacAddress();
  //Serial.println(WiFi.macAddress());
  WiFi.disconnect();

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

  // Set ESP-NOW Role
  esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
    
  // Register for a callback function that will be called when data is received
  esp_now_register_recv_cb(OnDataRecv);
  esp_now_register_send_cb(OnDataSent);

  pairingData.id = 2;
}
 
void loop() { 
  if (autoPairing() == PAIR_PAIRED) { 
    static unsigned long lastEventTime = millis();
    static const unsigned long EVENT_INTERVAL_MS = 10000;
    if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
      Serial.print(".");
      getReadings();

      //Set values to send
      myData.msgType = DATA;
      myData.id = 2;
      myData.temp = temperature;
      myData.hum = humidity;
      myData.readingId = readingId ++;
      
      // Send message via ESP-NOW to all peers 
      esp_now_send(pairingData.macAddr, (uint8_t *) &myData, sizeof(myData));
      lastEventTime = millis();
    }
  }
}

View raw code

How the Code Works

The ESP32 and ESP8266 are slightly different when it comes to the ESP-NOW-specific functions. But they are structured similarly. So, we’ll just take a look at the ESP32 code.

We’ll take a look at the relevant sections that handle auto-pairing with the server. The rest of the code was already explained in great detail in a previous project.

Set Board ID

Define the sender board ID. Each board should have a different id so that the server knows who sent the message. Board id 0 is reserved for the server, so you should start numbering your sender boards at 1.

// Set your Board ID (ESP32 Sender #1 = BOARD_ID 1, ESP32 Sender #2 = BOARD_ID 2, etc)
#define BOARD_ID 1

Define the maximum number of channels

The sender will loop through different Wi-Fi channels until it finds the server. So, set the maximum number of channels.

#define MAX_CHANNEL 11  // for North America // 13 in Europe

Server’s MAC Address

The sender board doesn’t know the server MAC address. So, we’ll start by sending a message to the broadcast MAC address FF:FF:FF:FF:FF:FF on different channels. When we send messages to this MAC address, all ESP-NOW devices receive this message. Then, the server will respond back with its actual MAC address when we find the right Wi-Fi channel.

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

Data Structure

Similarly to the server code, we create two structures.One to receive actual data and another to receive details for pairing.

//Structure to send data
//Must match the receiver structure
// Structure example to receive data
// Must match the sender structure
typedef struct struct_message {
  uint8_t msgType;
  uint8_t id;
  float temp;
  float hum;
  unsigned int readingId;
} struct_message;

typedef struct struct_pairing {       // new structure for pairing
    uint8_t msgType;
    uint8_t id;
    uint8_t macAddr[6];
    uint8_t channel;
} struct_pairing;

//Create 2 struct_message 
struct_message myData;  // data to send
struct_message inData;  // data received
struct_pairing pairingData;

Pairing Statues

Then, we create an enumeration type called ParingStatus that can have the following values: NOT_PAIRED, PAIR_REQUEST, PAIR_REQUESTED, and PAIR_PAIRED. This will help us following the pairing status situation.

enum PairingStatus {NOT_PAIRED, PAIR_REQUEST, PAIR_REQUESTED, PAIR_PAIRED,};

We create a variable of that type called pairingStatus. When the board first starts, it’s not paired, so it’s set to NOT_PAIRED.

PairingStatus pairingStatus = NOT_PAIRED;

Message Types

As we did in the server, we also create a MessageType so that we know if we received a pairing message or a message with data.

enum MessageType {PAIRING, DATA,};
MessageType messageType;

Adding a Peer

This function adds a new peer to the list. It accepts as arguments the peer MAC address and channel.

void addPeer(const uint8_t * mac_addr, uint8_t chan){
  esp_now_peer_info_t peer;
  ESP_ERROR_CHECK(esp_wifi_set_channel(chan ,WIFI_SECOND_CHAN_NONE));
  esp_now_del_peer(mac_addr);
  memset(&peer, 0, sizeof(esp_now_peer_info_t));
  peer.channel = chan;
  peer.encrypt = false;
  memcpy(peer.peer_addr, mac_addr, sizeof(uint8_t[6]));
  if (esp_now_add_peer(&peer) != ESP_OK){
    Serial.println("Failed to add peer");
    return;
  }
  memcpy(serverAddress, mac_addr, sizeof(uint8_t[6]));
}

Receiving and Handling ESP-NOW Messages

The OnDataRecv() function will be executed when you receive a new ESP-NOW packet.

void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {

Inside that function, print the length of the message:

Serial.print("Packet received with ");
Serial.print("data size = ");
Serial.println(sizeof(incomingData));

Previously, we’ve seen that we can receive two types of messages: PAIRING and DATA. So, we must handle the message content differently depending on the type of message. We can get the type of message as follows:

uint8_t type = incomingData[0];       // first message byte is the type of message

Then, we’ll run different codes depending if the message is of type DATA or PAIRING.

If it is of type DATA, copy the information in the incomingData variable into the inData structure variable.

memcpy(&inData, incomingData, sizeof(inData));

Then, we simply print the received data on the Serial Monitor. You can do any other tasks with the received data that might be useful for your project.

Serial.print("ID  = ");
Serial.println(inData.id);
Serial.print("Setpoint temp = ");
Serial.println(inData.temp);
Serial.print("SetPoint humidity = ");
Serial.println(inData.hum);
Serial.print("reading Id  = ");
Serial.println(inData.readingId);

In this case, we blink the built-in LED whenever the reading ID is an odd number, but you can perform any other tasks depending on the received data.

if (incomingReadings.readingId % 2 == 1){
  digitalWrite(LED_BUILTIN, LOW);
} else { 
  digitalWrite(LED_BUILTIN, HIGH);
}
break;

If the message is of type PAIRING, first we check if the received message is from the server and not from another sender board. We know that because the id variable for the server is 0.

case PAIRING:    // we received pairing data from server
  memcpy(&pairingData, incomingData, sizeof(pairingData));
  if (pairingData.id == 0) {              // the message comes from server

Then, we print the MAC address and channel. This information is sent by the server.

Serial.print("Pairing done for MAC Address: ");
 printMAC(pairingData.macAddr);
 Serial.print(" on channel " );
 Serial.print(pairingData.channel);    // channel used by the server

So, now that we know the server details, we can call the addPeer() function and pass as arguments the server MAC address and channel to add the server to the peer list.

addPeer(pairingData.macAddr, pairingData.channel); // add the server  to the peer list 

If the pairing is successful, we change the pairingStatus to PAIR_PAIRED.

pairingStatus = PAIR_PAIRED;             // set the pairing status

Auto Pairing

The autoPairing() function returns the pairing status.

PairingStatus autoPairing(){

We can have different scenarios. If it is of type PAIR_REQUEST, it will set up the ESP-NOW callback functions and send the first message of type PAIRING to the broadcast address on a predefined channel (starting at 1). After that, we change the pairing status to PAIR_REQUESTED (it means we’ve already sent a request).

case PAIR_REQUEST:
  Serial.print("Pairing request on channel "  );
  Serial.println(channel);

  // set WiFi channel   
  ESP_ERROR_CHECK(esp_wifi_set_channel(channel,  WIFI_SECOND_CHAN_NONE));
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
  }

  // set callback routines
  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
    
  // set pairing data to send to the server
  pairingData.msgType = PAIRING;
  pairingData.id = BOARD_ID;     
  pairingData.channel = channel;
  pairingData.macAddr[0] = clientMacAddress[0];
  pairingData.macAddr[1] = clientMacAddress[1];
  pairingData.macAddr[2] = clientMacAddress[2];
  pairingData.macAddr[3] = clientMacAddress[3];
  pairingData.macAddr[4] = clientMacAddress[4];
  pairingData.macAddr[5] = clientMacAddress[5];

  // add peer and send request
  addPeer(serverAddress, channel);
  esp_now_send(serverAddress, (uint8_t *) &pairingData, sizeof(pairingData));
  previousMillis = millis();
  pairingStatus = PAIR_REQUESTED;
  break;

After sending a pairing message, we wait some time to see if we get a message from the server. If we don’t, we try on the next Wi-Fi channel and change the pairingStatus to PAIR_REQUEST again, so that the board sends a new request on a different Wi-Fi channel.

case PAIR_REQUESTED:
  // time out to allow receiving response from server
  currentMillis = millis();
  if(currentMillis - previousMillis > 250) {
    previousMillis = currentMillis;
    // time out expired,  try next channel
    channel ++;
    if (channel > MAX_CHANNEL){
     channel = 1;
    }   
    pairingStatus = PAIR_REQUEST;
  }
  break;

If the pairingStatus is PAIR_PAIRED, meaning we’re already paired with the server, we don’t need to do anything.

case PAIR_PAIRED:
  // nothing to do here 
  break;

Finally, return the pairingStatus.

return pairingStatus;

setup()

In the setup(), set the pairingStatus to PAIR_REQUEST.

pairingStatus = PAIR_REQUEST;

loop()

In the loop(), check if the board is paired with the server before doing anything else.

if (autoPairing() == PAIR_PAIRED) {

This will run the autoPairing() function and handle the auto-pairing with the server. When the board is paired with the sender (PAIR_PAIRED), we can communicate with the server to exchange data with messages of type DATA.

Sending Messages to the Server

In this case, we’re sending arbitrary temperature and humidity values, but you can exchange any other data with the server.

unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
  // Save the last time a new reading was published
  previousMillis = currentMillis;
  //Set values to send
  myData.msgType = DATA;
  myData.id = BOARD_ID;
  myData.temp = readDHTTemperature();
  myData.hum = readDHTHumidity();
  myData.readingId = readingId++;
  esp_err_t result = esp_now_send(serverAddress, (uint8_t *) &myData, sizeof(myData));
}

Testing the Sender Boards

Now, you can test the sender boards. We recommend opening a serial communication with the server on another software like PuTTY for example so that you can see what’s going on on the server and sender simultaneously.

After having the server running, you can upload the sender code to the other boards.

After uploading the code, open the Serial Monitor at a baud rate of 115200 and press the RST button so that the board starts running the code.

This is what the sender should return.

ESP32 Client ESP-NOW Protocol Autopairing Arduino IDE Serial Demonstration

As you can see, first, it sends a pairing request using different channels until it gets a response from the server. In this case, it is using channel 7.

After that, we start receiving messages from the server. We also send messages to the server.

On the server side, this is what happens:

ESP32 Server ESP-NOW Protocol Autopairing Arduino IDE Serial Monitor Receive Data Demonstration

The server receives a pairing request from the sender. It will pair with the sender. In my case, it was already paired because I had run this code before. After data, we start sending and receiving data.

You can upload the sender code to multiple boards and they will all automatically pair with the server. The sender boards can be ESP32 or ESP8266 boards. Make sure you use the right code for the board you’re using.

Now, you can go to the server’s IP address to see the readings from the sender boards displayed on the dashboard. The web page is prepared to display readings from two boards. If you want to display more readings you need to modify the web page.

ESP-NOW web server dashboard

Wrapping Up

In this tutorial, we’ve shown you how you can build a ESP-NOW Web Server, pair with peers automatically and establish a two-way communication between server and senders boards.

You can adapt the parts of code that deal with auto-pairing and use them in your ESP-NOW examples.

We would like to thank Jean-Claude Servaye for sharing his ESP-NOW auto-pairing code sketches with us. We only made a few modifications to the sketches. You can find the original codes on his GitHub page.

You may also like:

Thanks for reading.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Recommended Resources

Build a Home Automation System from Scratch » With Raspberry Pi, ESP8266, Arduino, and Node-RED.

Home Automation using ESP8266 eBook and video course » Build IoT and home automation projects.

Arduino Step-by-Step Projects » Build 25 Arduino projects with our course, even with no prior experience!

What to Read Next…


Enjoyed this project? Stay updated by subscribing our newsletter!

106 thoughts on “ESP-NOW: Auto-pairing for ESP32/ESP8266 with Bidirectional Communication and Web Server”

  1. In the “autoPairing()” function for the ESP32 sender, there is a small bug.
    At NOT_PAIRED status when ‘esp_home_init()’ is false you report this and continue trying to pair on that channel.
    Should the logic action not be to return to the loop with the NOT_PAIRED status?

    Reply
    • Just one more thing. ESP-NOW is a very open protocol there is not so much of package validation, when multiple setup are using ESP-NOW for communication you can easily interfere with other setups, especially when the other setups don’t check what kind of package is being received and of it fits in there data structure.

      Best solution imho is adding an unique package identifier one that only is being know to your setup. And when this identifier is not received just ignore the package until a valid package is received.
      Imho, this should be something that esp-now should handle it self. So it is present in every setup out of the box.

      Reply
  2. I think when you send data over ESPnow to the broadcast address, there is no way to have a success confirme – only if you use unicast then you will have real confirmation that the receiver received the data you sent

    Reply
  3. This is just brilliant!
    It will make your devices even more user friendly! And it’s perfect to add more slaves in the future!

    Reply
  4. Hi,
    very interesting and as usual very well documented, but i can’t compile the server as ESP8266, only as ESP32.
    It don’t accect neither include <esp_now.h> nor include <espnow.h>.
    Bye
    Renzo

    Reply
    • Hi.
      This example uses ESP32 as server. The code won’t work for ESP8266.
      ESP32 and ESP8266 use different ESP-NOW libraries and use different functions.
      For the slaves we provide the code for ESP32 and for ESP8266.
      Regards,
      Sara

      Reply
  5. Very good, it worked on first try. I see the ESP-NOW can work over long distance of 250 meters, and there is also a Long-Range settings that will lett it work uptill 500 meters.
    I was planing to use one of the units in my garage. It is like a bunker with concreat walls and metal gate, so the signals bearly gets out.
    I wanted to test this project inLong-Raangemode, but I am not sure what functions to modify.
    If you could help in that it would be very good.

    Reply
    • I saw on ESP page this one:

      esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B|WIFI_PROTOCOL_11G|WIFI_PROTOCOL_11N|WIFI_PROTOCOL_LR);

      but honestly speaking I don’t see any improvement

      Reply
    • There is a lot more to LR than just the range.
      Only available to ESP33
      The call only works when you bypass the Arduino wrapper and talk directly to the espresif language
      Channel speed changes which will effect the webserver
      Make a repeater of use a Lora module for distance.
      I found all this, the hard way.

      Reply
  6. Maybe Long-Range is easy to change to.

    //here config LR mode
    int a= esp_wifi_set_protocol( WIFI_IF_AP, WIFI_PROTOCOL_LR );

    Maybe it is that simple, just change to: WIFI_PROTOCOL_LR

    I do not know, but I willtest it and see if that will do the difference.

    Reply
  7. This is very interesting and clever project. I want to use it for a greenhouse monitoring/watering operation. My problem is the JSON code. I really do not want to upgrade to the json 6 library as it will break all my older projects. Any hints on how to down convert the json specific code to use the json 5 library ? Thanks. I will subscribe to rnt lab later today as I found your work to be useful and I want to contribute.

    Reply
  8. Could not get sender code to work. Using M5ATOM. Here is the runtime error:
    Client Board MAC Address: 24:A1:60:46:27:9C
    Pairing request on channel 1
    E (2180) wifi: esp_wifi_set_channel 1453 wifi not start or sniffer not enabled, should start wifi or enable sniffer first
    ESP_ERROR_CHECK failed: esp_err_t 0x3002 (ESP_ERR_WIFI_NOT_STARTED) at 0x4008c918
    file: “C:\Users\vsing\Documents\Arduino Programs\ESP32nowTest\ESP32sender_example\ESP32sender_example.ino” line 161
    func: PairingStatus autoPairing()
    expression: esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE)

    Backtrace: 0x4008c434:0x3ffb1f30 0x4008c91b:0x3ffb1f50 0x400d1188:0x3ffb1f70 0x400d12d3:0x3ffb1f90 0x400d26cd:0x3ffb1fb0 0x40088b7d:0x3ffb1fd0

    Rebooting…

    Maybe you know why?

    Reply
  9. I have compiled and uploaded the codes to ESP32 and ESP8266
    the systems keeps hanging into the autopairing state
    On the ESp32-side I receive messages like
    14:26:30.548 -> Last Packet Send Status: Delivery Success
    14:26:30.899 -> Packet received from: 2c:3a:e8:22:62:30
    14:26:30.934 -> data size = 4

    but the ESp8266 keeps on looping doing pairing requests

    Reply
    • OK. finally found the bug. I was uploading the “send-sensor-data”-code to the ESP32.
      This means both the ESP8266 and the ESP32 where running the “send-sensor-data”-code
      both trying to connect to a “receive-data”-device which is the ESP32 that acts as data-receiver and webserver.

      After uploading the “receive-data/Webserver”-code to the ESP32 it works now.

      Reply
  10. Hi Rui and Sara,

    As usual, perfect tutorial. My setup works perfectly with a M5Stick-C as the master and two remotes (nodemcu) each having a DHT11 sensor. I have one Nodemcu in my shed and one nodemcu in my living room.

    Concerning this comment in your code:
    //Must match the receiver structure
    // Structure example to receive data
    // Must match the sender structure

    If I was to add another remote (ie: Wemos) which read the intensity of daylight in a bedroom (A0 <— LDR):

    1- I will have to modify the data structure to add a variable name (ie: int intensityValue), including the already Temperature and Humidity variables.
    2- This variable as to be included in all Remotes and the Master ?
    3- That means I’ll have to recompile and reprogram the M5Stick and 2xNodemcu with this new variable.
    4- This seem to be a show stopper for an easy network expansion, is my reasonning wrong ?

    Maybe I’m missing something and that there is more with defining the data structure/architecture for future expansion, before coding 🙂

    Keep up the good work !

    Regards,
    Mario

    Reply
    • Hi.
      You need to create another new structure in the server that includes that variable.
      That new structure must be the same in that new sender board and the server. You don’t need to change other senders.
      Make sure you add something to the structure that identifies that data is coming from that specific board so that the sender knows what structure to expect and how to handle the data.
      I hope this helps.
      Regards,
      Sara

      Reply
  11. Hi everyone and thanks for your work.
    I m using the “Doit esp32 DevKit V1”

    And when I test your code (server mod) it loops returning status 6.

    while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println(“Setting as a Wi-Fi Station..”);
    }

    Is this a compatibility issue with my card? Can you help me?
    Thanks in advance 🙂

    Reply
  12. I encounter the same problem on time. I use platformio and the problem was solved after erasing flash and re uploading code …

    Reply
  13. Does this keep track of the channel numbers each device is on. I have noticed connecting to servers can be a different channel than the esp-now devices. Caused me no end of trouble. Before trying this system, i thought i would ask.
    You paid for books are great. If people haven’t purchased any, they are worth the money, even if out exchange rate where i live sucks 🙂

    Reply
    • for wifi and espnow to work simultaneously both have to use the same channel.
      if you have a router/wireless access point that changes the channel depending on the pollution, then you need to apply proper logic on the espnow node
      what I am doing is:
      – have a file where you save the channel on the node
      – when you start esp32 read the channel from the file
      – send espnow message on this channel
      – if failed, go through all channels (from 1 to 13)
      – the first channel that data is sent with success, save this channel to the file for the next use
      – end of story

      of course it would be easier if your wifi router does NOT change the channel but sometimes it is impossible to get

      Reply
      • I will try that. The server is the ESP32. I have tried setting the channel number, however the internet is full of old outdated into. I have spent days on resolving this. Even wired issues where esp32-s3 seems to mess up the network, causing it to stop.

        Reply
          • there is more in this function as I have 3 gateways around the house that are all listening to the sensor devices, so in case sending to 1st gateway fails, it tries to send to 2 others as well that is why this function is quite complicated – but I think it is well commented to understand it

          • I want to congratulate you on the project you published on Github, it is incredibly well documented with clear pictures and diagrams. I found it very inspirational and a very imformative read. I will certainly be using some of your examples. Did you share the gerbers for the ESP32-S2 PCB’s anywhere? Or better stil ldo you sell complete boards? Based on the view of power sockets, are you based in the Uk?

          • Thank you Bob.
            The gerber files are inside the folder:
            https://github.com/homonto/ESPnow-MQTT-HomeAssistant-Gateway/tree/master/1-pcb/easyeda

            it is not even gerber but the json for EasyEda so you can open it there (even online version of EasyEDA can do it) and modify as you wish and then do the gerber thingis
            Yes, I am living in UK.
            I don’t sell the boards but since I ordered a lot if you wish can send send you few of them – unsoldered – just PCB – for free 😉
            let me know if you want: [email protected]

      • I recommend a change to the code to #DEFINE nChannels = 11 // or 13 as required for your country. The server can report :
        Serial.print(“Wi-Fi Channel: “);
        Serial.println(WiFi.channel());
        when WiFi connects.

        Reply
    • Hi Terry.
      With this example you don’t need to set any channel.
      Everything is done automatically by trial and error in the code.
      Regards,
      Sara

      Reply
      • Some countries (e.g Australia) have 13 channels.

        around line 188 in the ESP8266 code you need:
        channel ++;
        if (channel > 11) {
        channel = 0;
        or better a #DEFINE nChannels = 11 // or 13 for countries that have 13.
        AND
        channel ++;
        if (channel > nChannels) {
        channel = 0;

        Reply
  14. Hello! I’ve been thinking a lot about using wifi and espnow at the same time but I have some doubts that I haven’t been able to answer no matter where I look… I know it is possible to use them together, but is it known if there is any loss in the espnow protocol? How could it affect “range”? Or does it lower performance (fewer successfully delivered packets)?

    What worries me most is the range at which it can be used, since I would need it especially for applications that make use of its extended range…

    Regards!

    Erik

    Reply
    • if you use ONLY ESPnow, you can enjoy longer range (“ESP-NOW supports long-distance communication”) because Espressif supports their own protocol called – see below:

      #if CONFIG_ESPNOW_ENABLE_LONG_RANGE
      ESP_ERROR_CHECK( esp_wifi_set_protocol(ESPNOW_WIFI_IF, WIFI_PROTOCOL_11B|WIFI_PROTOCOL_11G|WIFI_PROTOCOL_11N|WIFI_PROTOCOL_LR) );
      #endif

      this is the one: WIFI_PROTOCOL_LR

      check their example here:
      https://github.com/espressif/esp-idf/blob/master/examples/wifi/espnow/main/espnow_example_main.c

      Reply
      • I have researched this and have read that the range of ESPNow under normal conditions is approximately 300 meters. However, there is an option called “long range” that allows for even further range, reaching over 500 meters.

        My question is if, when using ESPNow and WiFi at the same time, the range of ESPNow without having activated the “long range” option remains the same, that is, around 300 meters. Thank you very much for your response.

        Anyone know about that?

        Erik

        Reply
        • “there is an option called “long range” ” – that is what I gave you, it is called: WIFI_PROTOCOL_LR and you set it up using esp_wifi_set_protocol()

          when you activate wifi with this protocol enabled (see the way it is done: prot1 || prot2 || so “OR”) your ESP will choose the one that the other side supports

          see the documentations on espressif site: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html

          “Call esp_wifi_set_protocol(ifx, WIFI_PROTOCOL_LR) to set the station/AP only to the LR mode.
          This mode is an Espressif-patented mode which can achieve a one-kilometer line of sight range. Please make sure both the station and the AP are connected to an ESP device.”

          Reply
          • hi everyone,
            I’m hoping to get some help with an esp32 project I’m working on. I have succesfully set up esp32 to esp32 communication with ESP-NOW and I want to try and increase the ESP-NOW range with WIFI_PROTOCOL_LR. I’ve been trying things and searching online for a while now, but I can’t get this to work. I’m using a FireBeetle DFR0478 and the Arduino IDE. I’m using the following but I’m not seeing any errors reported and I’m also not seeing any increase in range. So it’s not clear if it’s working or not. I’m really new to this, so I really appreciate any help or suggestions. Thanks!

            WiFi.mode(WIFI_STA);
            //Turn on LR mode
            ESP_ERROR_CHECK(esp_wifi_set_protocol( WIFI_IF_STA, WIFI_PROTOCOL_LR ));

            Steve

  15. there is also a nice table showing all the possible scenarios:

    According to the table, the following conclusions can be drawn:

    “For LR-enabled AP of ESP32, it is incompatible with traditional 802.11 mode, because the beacon is sent in LR mode.

    For LR-enabled station of ESP32 whose mode is NOT LR-only mode, it is compatible with traditional 802.11 mode.

    If both station and AP are ESP32 devices and both of them have enabled LR mode, the negotiated mode supports LR.”

    btw don’t look for someone to tell you if you have all the info on the vendor of ESP site – someone will tell you his interpretation – check at the source 😉

    Reply
  16. Hoping someone can help me. This works great with two ESP32 boards, but when I try with an 8266 as a Sender I see this on the serial monitor of the Server:
    Server MAC Address: 40:22:D8:EE:63:0C
    Setting as a Wi-Fi Station..
    Server SOFT AP MAC Address: 40:22:D8:EE:63:0D
    Station IP Address: 192.168.1.171
    Wi-Fi Channel: 2

    This is a new 8266 Pairing:

    9 bytes of data received from : c8:c9:a3:5d:8d:2e
    0
    2
    Pairing request from: c8:c9:a3:5d:8d:2e
    2
    send response
    Pair success
    9 bytes of data received from : c8:c9:a3:5d:8d:2e
    0
    2
    Pairing request from: c8:c9:a3:5d:8d:2e
    2
    send response
    Already Paired
    Last Packet Send Status: Delivery Fail to c8:c9:a3:5d:8d:2e
    9 bytes of data received from : c8:c9:a3:5d:8d:2e

    And the 8266 Sender keeps looping through Pairing Requests on the channels: so the Server registers it as a peer, but the 8266 does not know it succeeded. At that point I am kind of doomed: subsequent restarts and the 8266 keeps asking to pair, and the ESP32 keeps saying “Already Paired”. Any help on why and how to fix would be appreciated!!

    And I am learning so much thanks to randomnerdtutorials!!

    Reply
  17. ESP8266 sender:
    You hard coded the channel to be max 11. That results in an infinit loop for users of channel 12 or 13.
    You should replace it with #define according to the ESP32 sender.

    Reply
  18. Thanks for the feedback, not sure that is it. The clip above shows the server on chan2, and I am in the US so have up to chan 11 only. I am a newbie and think the issue has to do with:
    “*ESP32 in WIFI_AP_STA mode responds with its WiFi.macAddress but it uses WiFi.softAPmacAddress to receive from ESP8266 peer.

    WiFi.softAPmacAddress is created from WiFi.macAddress by adding 1 to the last byte—check the documentation.”
    But I really do not understand the difference?

    Has anyone got this working sending from an 8266 to an ESP32? Thanks!

    Reply
  19. One of my boards is BOARD 1 and the other is BOARD 2.
    When I launch the browser, I only get information from BOARD 2, regardless of whether both boards are on or just one of them…

    Reply
  20. Hi,
    I have got this working and everything is fine but one question.
    The access point generated by the receiver / server does not have any security.
    is this a problem ? and if so is there any way round it ?

    Thanks

    Reply
  21. I’ve got 2 ESP32s, one I’m using as receiver and the other as sender. The receiver code compiled and installed on the server. The sender code won’t compile. I’m getting the following error message. Compilation error: enumeration value ‘NOT_PAIRED’ not handled in switch [-Werror=switch]

    Reply
      • I did not change the code that you provided.
        Except I added a case statement called NOT_PAIRED which is empty. Then I had to modify line 281 which says “esp_err_t result = esp_now_send(serverAddress, (uint8_t *) &myData, sizeof(myData));”
        The code would not compile because ‘result’ is undeclared. So at first I just commented out this line until I realized that this is the line that does the actual send. I removed ‘esp_err_t result = ‘ from the code and it is working fine. I also added code to read my DHT22 sensor.

        Reply
  22. Hello,

    first of all I want to explain that I use capital letters not to shout but only to emphasize certain aspects. Keep this in mind if you read CAPITALISED words.
    Thank you.

    I did successfully test your code.
    Though understanding the code is still hard even with your explanations.
    I will explain why:

    You are using the terms server, peer, station-mode, AP-mode.
    If you are unexperienced with this words it is hard to imagine which ESP-board is what.
    additionally the names of variables are still not SPOT_ON to say what they are
    example
    pairingData.macAddr
    is this mac-adress the Servers MAC-adress or the Senders-MAC-Adress ?????????

    This is what makes it HARD to understand the details of how the code works.

    These names should EXPLICITLY say whom’s mac-adress this is and what the purpose is
    and if it is the Station-mode-adress or the AP-mode-adress
    in the sense of saying the following things:
    is it a MAC-adress USED for SENDING to a DIFFERENT board
    is it a MAC-Adress that tells WHAT kind of board “it is my STA-MAC-Adress or AP-MAC-Adress

    If the name shall say all this the name will become pretty long I know. But then the variable name is really SPOT-ON SELF-explaining!

    I want to adapt your code for using in combination with elegantOTA, ESP-DASH and ESP-Webserial
    without autodetecting and for this I have to understand EACH and EVERY SINGLE detail of your code to understand what to keep and what to delete.

    Same thing with peers. Both sides have to add peers but you only use the word “peer” without cleary identifying is it an entry in its OWN peer-list or is it data that the partner shall add in the PARTNERs peer-list

    If you could add an example that just shows all the steps and conditions how to use
    AsyncWebservers and ESP-NOW on a NON-default WiFi-channel this would save me estimated 10 hours of time

    This example shall EXPLICITLY not use the autodetect mechanism but just
    define a HARDCODED WiFi-channel and show how to setup
    The Webserver-boatd (with only STA or STA_AP-mode?)
    and how SENDING units are setup

    Again all my CAPITALISED words are just ment as emphasizing not – really really not as shouting.

    Thank you very much in advance for answering
    best regards Stefan

    Reply
  23. Hi,
    First of all, thank you for this beautiful tutorial, it helped me a lot while setting up my ESP-NOW based sensor network.
    I’d like to ask one thing though. I’ve been trying to modify the code such that upon “Delivery Failed” status (only when I’m sending measurements, not pairing requests) the device runs auto-pairing again, but I’ve been struggling to do that. I’ve tried manually changing pairingStatus to PAIR_REQUEST when some conditions are met, but it looks like I’m missing something. The device enters pairing loop when data fails to be delivered, but even though the server is turned on again, pairing packets all return “Delivery Failed” status, even though I can see that the server received pairing requests and responded with pairing structure.
    Can you think of any way to run autoPairing() from within the code in this case?
    Regards,
    Mike

    Reply
  24. Hello, thanks for the work.

    I left a comment yesterday but it wasn’t approved. May be because was too long.
    I wanted to share that i solved the problem that when you reset a slave node, the pairing starts again. This was solved by adding “addPeer(mac_addr);” at the start of OnDataRecv callback function.

    I wasn’t able to solve another main bug: The code takes a very long time (about 60 seconds) to do the pairing because it continues changing channel even if the serial monitor says that it’s paired. Do you have a workaround to solve this?

    Reply
  25. Hola Rui y Sara, muchas gracias, está entretenido el código.

    Lo apliqué en un sistema con tres placas ESP32 más sensor BMP280 midiendo temperatura, humedad y presión que muestran en unas pantallas OLED y entregándome todo en WEB con una cuarta placa sin problemas. Pero tengo solo un inconveniente que no puedo encontrar. Tras un cierto número de envíos de datos de temperatura, humedad y presión la transmisión de datos se cancela no duran más de 24 horas (no sé la causa), no sigue transmitiendo, pero los sensores siguen haciendo su trabajo. Para solucionarlo tengo que reiniciar las cuatro placas, solo con eso vuelven a funcionar todo normamente.
    Me podrían ayudar a entender donde puede estar el problema, por favor.

    Reply
  26. Hi, First of all, thanks for this interesting tutorial.
    My project is now working fine with WIFI and ESPNOW.
    However, sometimes my project need to run in environment where there is no WIFi router, so no WiFi available. In this case, pairing of 2 ESP32 boards with ESPNow seems not to be possible. Indeed, no WiFi channel can be shared.
    Do you have a workaround so that the 2 SP32 board still connect through ESPNow, even if no WiFi is available ?

    Reply
      • Hi Dave,
        Thanks for your response.
        However, I need to pair with ESPNow without using MAC addresses.
        In the meantime, I perhaps found the solution by modifying a little bit the code. I am testing it right now to see if the solution is viable on the longer term. If I am OK with the code modifications, I will share them.

        Reply
        • Hi,
          By chaning a little bit the pairing process, I solved the issue.
          When there is no WiFi anymore, we need to specify the WiFi channel by our own so that it correspond with the proposed WiFi channel of the server.
          Herewith the code change:

          case PAIRING: // the message is a pairing request
          memcpy(&pairingData, incomingData, sizeof(pairingData));
          printMAC(mac_addr);
          if (pairingData.id > 0) { // do not replay to server itself
          if (pairingData.msgType == PAIRING) {
          pairingData.id = 0; // 0 is server
          // Server is in AP_STA mode: peers need to send data to server soft AP MAC address
          WiFi.softAPmacAddress(pairingData.macAddr);
          if (WiFi.status() != WL_CONNECTED) {
          Serial.println (“WiFi not connected”);
          WiFi.disconnect();
          WiFi.begin(ssid, password, pairingData.channel,NULL,false);
          WiFi.disconnect(); //we do not want to connect to a WiFi network
          pairingData.channel = WiFi.channel();
          chan = WiFi.channel();
          slave.channel = WiFi.channel();
          }
          if (WiFi.status() == WL_CONNECTED) {
          pairingData.channel = chan;
          }

          printMAC(mac_addr);

          addPeer(mac_addr);
          Serial.println ("peer added");
          esp_err_t result = esp_now_send(mac_addr, (uint8_t *) &pairingData, sizeof(pairingData));
          //esp_now_send(mac_addr, (uint8_t *) &pairingData, sizeof(pairingData));
          Serial.println ("end of pairing process");
          }
          }
          break;

          Reply
  27. void readDataToSend() {
    outgoingSetpoints.msgType = DATA;
    outgoingSetpoints.id = 0;
    outgoingSetpoints.temp = random(0, 40);
    outgoingSetpoints.hum = random(0, 100);

    If I am using IMU sensor what could be the syntax in this function?

    Reply
    • Minisha,
      you need to search for the actual IMU library.
      e.g.
      ”’#include <Arduino_LSM6DSOX.h>
      ……
      void loop() {
      float x, y, z;

      if (IMU.accelerationAvailable()) {
      IMU.readAcceleration(x, y, z);

      …. then something like:
      outgoingSetpoints.X_axis = x;
      outgoingSetpoints.Y_axis = y;
      outgoingSetpoints.z_axix = z;
      ”’

      Reply
  28. I am using many MPU6050 and esp32. What could be the code for calling all the sensors data at a time and need to save the file?

    Reply
    • You need to think in small steps so you can code each step at a time.
      1) you need a library for the MPU6050. In Library Manager, you search for a suitable library (I’d recommend the Adafruit one because they have some good tutorials too) and try their examples.
      e.g. Basic Readings will print the outputs on the Serial Monitor.
      2) Your next problem is where do you want to ‘save the file’? to the cloud? on a memory card? print them a WWW page?
      You have to think about that and look for examples that will do that part of what you need.

      Also, since this tutorial is about ESP’s and ESP-now, it is probably NOT the place to be asking about code for sensors.

      Dave

      Reply
  29. Hi Guys,

    Thank you for all your hard with with the tutorials, very mech appreciated. I have implemented this tutorial and it works perfectly, I even tried the suggested workaround for now WiFi router offered by Guy Melon above. All good!

    I would like to have the devices auto-pair only if a device ID and Mac address do not already exist in non-volatile memory, so something like –
    – On start up check EEPROM/Preferences to see if ID/Mac pair exist
    – If exist start communications
    – If not exist enable auto-connect (maybe button to initiate)
    – Pass found ID/Mac pair to EEPROM

    Currently there is the ability to store the Channel number and chick it, but I seem to be having trouble saving the Mac address to EEPROM.

    Has anyone managed anything like this?

    Regards

    Damien

    Reply
  30. Is it possible to add 2 way communication between 2 “slaves” or “peers” in this scenario? I currently have a Master (Board 0), with 2 peers(Board 1 and Board 2). I would like to be able to have one of the peers (board 1) act the master function to the second peer(Board 2), as it is very possible the “Master” might get out of range of the peers, and peer1 has a LCD Display that I would like to use to display data from peer2.

    Reply
  31. Hi,
    I just noticed in your code the following emum:
    enum MessageType {PAIRING, DATA,};
    MessageType messageType;
    However, messageType is not used anywhere in your code. What is the purpose of this enum ?

    Later in the code we can see in void OnDataRecv :
    uint8_t type = incomingData[0]; // first message byte is the type of message
    switch (type)
    Should type not be replaced by messageType ?

    Reply
  32. I am wondering if it’s possible to use the esp32 sketch as a ESP8266 version.

    But as the esp8266 uses espnow.h instead of esp32’s esp_now.h

    Idk if it would be possible but would be a really cool addition to have to only use an esp8266 for this.

    part of the issue is that the esp8266 espnow version doesnt use esp_err_t and esp_now_peer_info_t and esp_now_send_status_t and more.

    some people fix this by just saying == 0 as the definitions for esp_now_add_peer and others for the esp8266 espnow version are just ints

    I think this would take a few days of work but could be possible?

    I ended up just getting some cheap esp32 d1 minis from aliexpress so I will be testing with those. 2$ a piece!

    Reply
  33. Love the tutorials here..

    However.. is there a tutorials that JUST covers ESP now and auto-pairing? (broadcast).. WITHOUT the wifi, async, JSON stuff needed (or -is- any of this needed? if just doing local ESP Now, and wanting to auto-pair.broadcast?)

    Reply
    • Follow up.
      The ‘receiver’ boards have their own (built-in) mac addresses (as do all ESP32 boards)..
      However, in your code example, the receiver, onDataRecv() function.. there is this code/portion:
      case PAIRING: // we received pairing data from server
      memcpy(&pairingData, incomingData, sizeof(pairingData));

      if (pairingData.id == 0) { // the message comes from server
      Serial.print("Pairing complete with 'sender': (mac address) ");
      printMAC(mac_addr);
      Serial.println();

      Serial.print("Sender provider mac address (channel): ");
      printMAC(pairingData.macAddr);
      Serial.println();

      Which returns this:
      Pairing complete with ‘sender’: (mac address) 08:d1:f9:d0:d4:20
      Sender provider mac address ????: 08:d1:f9:d0:d4:21
      on channel 1 in 33ms

      My receiver board has a mac-address of:
      Client Board MAC Address: 08:D1:F9:D0:5D:94
      Pairing request on channel 1

      So why/where is this mac address coming from? (from the main/sender board)
      08:d1:f9:d0:d4:21

      This completely confusing me? Can anyone clarify/educate me on this?

      thanks!

      Reply
      • The struct that is sent by the server contains the Server Mac Address (pairingData.macAddr), and the client displays it when the paring is complete.
        I also found the code a little confusing.
        Dave

        Reply
        • ? Sorry can you clarify a bit more (I think sometimes the words confuse me.. wifi, server…etc) I understand the struct is sending this data…
          The main/sender (is this what you are referring to as the ‘server’?.. or this more about the code, and not the built in mac address?) As the board itself (when running the GetMacAddress sketches.. has a mac address of: 08:d1:f9:d0:d4:20.. which is sent in the struct under: printMAC(mac_addr); but it also is sending: 08:d1:f9:d0:d4:21 from printMAC(pairingData.macAddr);

          So what is the difference between the 2 then? (both coming from the same board)

          thanks!

          Reply
          • Yes, they are both coming from the main/server board.
            Without digging deeply I THINK one will be a built in address (xxx:20) and the other (xxx:21)created when the (Soft) Access Point is created. The difference would allow code in the board decide whether incoming network packets(messages) came from a ‘normal’ network like your home WiFi, or someone connecting to the WWW server via the board’s Soft Access Point.

            Dave

    • THANK YOU!… Finally (someone cared enough to -really- look and answer!) Really appreciate the reply and link! I think I need to read a couple more times! LOL but this is definitely the answer. +1

      Reply
      • Yes, Espressif docs (like many ‘Read the Docs’ pages) are what I’d call ‘clinical’. They describe the options etc. but now much of the ‘how’, ‘why’ or example uses. Their assumption would be that you have learned how to program and understand the technology before you come looking at their pages.
        I have some formal training from several years ago, but follow RNT (and other places) for their tutorials to fill the gaps.
        Dave

        Reply
  34. After playing with this tutorial example (and trimming a lot off to just focus ESP-NOW and no server/JSON/web-page display) I learned a lot from here now. Appreciate it..not -just- the tutorials, but community helpers as well.

    (Assuming I didnt mess anything during my edits, which I doubt from the diagrams and one way communication arrows for the pairing)

    I noticed that if the main/sender boards reboots.. the receivers still think they are ‘paired’?’
    Even when -just- running this in the loop()

    //one time check/output during loop..
    if (autoPairing() == PAIR_PAIRED){
    digitalWrite(LED_BUILTIN, HIGH);
    //if(initialPairState == 0){
    //initialPairState = 1;
    //}

    }else {
    digitalWrite(LED_BUILTIN, LOW);
    //pairingStatus = PAIR_REQUEST;
    initialPairState = 0;
    }

    Any tutorials about re-auto-pairing? or action being sent by main/sen boardder board upon reboot? Also curious as how to properly handle check for current pairing status?

    Update: after testing with several ‘peer/receiver’ devices I noticed the only the micro usb devkit board is -not- turning off built-in led when main/sender is powered down. The other devkit boards as USB-C ones? (same as main/sender board)? Why would this effect anything? Would it be different default firmware/version being different? (not sure how to check or even if would be issue causing this?) other boards turn off built-in led when main board is off..turns back on once main board is on again.. this other micro-usb board will not see when main board is off/not connected?All receiver boards run same code. How can physical board be a difference/cause here?

    Thanks

    Reply
  35. Update:
    Board type usb-c/micro usb did -not- matter.

    Seemed more so be the first vs second in peer list?
    Got very random results where things would be connected. Sometimes only one boards built in led would turn off. Seldomly both boards leds would turn off when main/sender board was un-plugged. Sometimes, when plugging in the main board again, lights would always go on..but only sometimes would actually take commands/data again. main board showed no mac addresses (when worked did show mac addresses of targeted peers)

    *previous post had name typo…(awaiting moderation still)

    Reply
  36. Hello what a wonderful Project
    I have a little Problem I can’t reach the Firebase Server when ESP_NOW is on.
    this is my code in the Loop function any clue to make this work
    Thank you Mr. Santos

    void loop() {
    static unsigned long lastEventTime = millis();
    static const unsigned long EVENT_INTERVAL_MS = 5000;
    if ((millis() – lastEventTime) > EVENT_INTERVAL_MS) {
    events.send(“ping”,NULL,millis());
    lastEventTime = millis();
    readDataToSend();
    esp_now_send(NULL, (uint8_t *) &outgoingSetpoints, sizeof(outgoingSetpoints));
    }
    PingTest();
    }

    void PingTest()
    {

    if(Ping.ping(remote_host,10)) {
    Serial.println(“Success!!”);
    float avg_time_ms = Ping.averageTime();
    Serial.printf(“+++++++++++++++++++++++++++++++++++++++++++Ping Average Time in ms : %f”,avg_time_ms);
    } else {
    Serial.println(“Error :(“);
    }

    }

    Reply
  37. Hello,
    Thank you for the example.
    Could you advise on how I may address a specific sender to control its’ GPIO and all of the senders at the same time?

    Thank you,
    Quang

    Reply
  38. Hello Sara and Rui,

    Like usual, you have made an Excellent work!!!!
    I have a question, were does the esp_now.h file is located? I did not install the esp-now library and it worked?
    Thanks a lot!!!!!

    Reply
  39. hello,

    as always, a high quality tutorial! one question, is it normal behaviour that after a reboot of the webserver esp32 all other esp need to be rebooted too to pair again? if yes, what would be a smart implementation to skip the pairing process for boards that already were paired and directly pair them again after the reboot of the webserver esp32?

    thank you for your time!

    Reply
  40. In the Arduino IDE, the ESP32 board software version 3.0.1 from Espressiv is currently up to date. The OnDataRecv method has different parameters than before. This leads to an error when compiling. The example here still compiles perfectly with the old ESP32-Board software version 2.0.17.

    Reply
  41. It seems that if the master is not using a router and is in SOFT_AP mode, the code itself can always specify the channel to use:
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(ssid, password, WIFI_CHANNEL);
    This is my experience with my small set of Sparkfun boards, but can anyone confirm this will always work no matter the ESP32 hardware?
    Thanks,
    Ron

    Reply
  42. Thanks for a very detailed and useful tutorial!

    However, I’m confused about the pairing routine on the server side.
    In the PAIRING case in the OnDataRecv() callback, it seems to me that you set up the server softAP mac adress to be equal to the mac adress of whichever slave is being paired at the moment (code pasted from your examples, comments are mine):

    //store incoming data pairingData variable
    memcpy(&pairingData, incomingData, sizeof(pairingData));
    //use the mac address from the incoming data to setup soft AP.
    WiFi.softAPmacAddress(pairingData.macAddr);

    Why do you do this? Wouldn’t it change the server mac address every time a new sender is paired? This would make the pairing info that any of the paired senders has stored obsolete as soon as new sender pairs with the server…

    Reply
  43. Hi,
    I’m using ESP32 DevKitC and VS Code with PlatformIO. I keep getting error message ‘class WiFiClass’ has no member named ‘STA’ when I try to compile your examples with WiFi.STA.begin(); line. What can be wrong?

    Reply
    • Hi,
      Which code are you trying to compile?
      Anyhow, I think that’s a typing error, the line should be just
      WiFi.begin();
      because the mode is set in the line above:
      WiFi.mode(WIFI_STA);

      Dave

      Reply
  44. Thanks for these setup. But I have one question, whatever I fill as board-id 1 or 2 I only get the board 2 data on the webserver, no board1.
    What did I wrong?

    Reply
  45. Tested the setup with one ESP32, and two ESP8266, that didn’t succeed.
    But with 3 ESP32 it was ok

    So the sketch for the ESP8622 must be the reason

    Reply
    • Harry, the code for ESP8266 is a bit different to the ESP32, due to different libraries. (see the include statements at the start).
      When you say it didn’t succeed, what were the exact messages or at what stage did it stop?
      Give us some screen shots of the serial monitor output.
      Dave

      Reply

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.