This guide shows how to create an ESP32 web server while using the ESP-NOW communication protocol. We will show you how to establish bi-directional communication between the master (web server) and the slaves and add boards to the network automatically (auto-pairing).

This tutorial is an extension to the following:
- ESP32: Tabelle des Sensors des Web-ESP-NOW (ESP-NOW + Wi-Fi)
The new version includes:
- Bidirectional communication between the server and the slaves;
- Auto Pairing Pairs: You don't need to know the MAC addresses of the boards. You don't have to add pairs manually. All you have to do is run the provided codes and the boards will be automatically added to the ESP-NOW network.
The improvements were suggested by one of our readers (Jean-Claude Servaye). The original codes can be found athis GitHub page.
If you are new to ESP-NOW, we recommend that you first familiarize yourself with the concepts and features of ESP-NOW. Check out the following guides to get started:
- Getting Started with ESP-NOW (ESP32 with Arduino IDE)
- Getting Started with ESP-NOW (ESP8266 NodeMCU with Arduino IDE)
- Bidirectional ESP-NOW communication between ESP32 boards
- ESP-NOW bi-directional communication between ESP8266 NodeMCU boards
Simultaneous use of ESP-NOW and Wi-Fi (web server).
There are a few things to consider if you want to use Wi-Fi to host a web server while also using ESP-NOW to get sensor readings from other boards:

- ESP32/ESP8266 Emitterplatinenmust use the same Wi-Fi channelas a receiving board (server).
- The WiFi channel of the receiving card is automatically assigned by your WiFi router.
- The receiving card's Wi-Fi mode must be access point and station (WIFI_AP_STA).
- You can set the same Wi-Fi channel manually, but we do it automatically. The sender tries different Wi-Fi channels until they get a response from the server.
project description
Here's a quick overview of the example we're going to create:

- There are two ESP sender cards (ESP32 or ESP8266) that send readings*via ESP-NOW to an ESP32 receiver board (ESP-NOW Many-to-One Configuration);
- The receiving board receives the packets and displays the readings on a web page;
- The webpage is automatically refreshed each time it receives a new read using Server-Sent Events (SSE).
- The receiver also sends data to the sender; this is intended to illustrate how bidirectional communication is established. As an example we will send any values, but you can easily replace them with sensor readings or other data like thresholds or GPIO enable/disable commands.
*we send random temperature and humidity readings; We will not use a real sensor. After you've tested the project and verified that everything works as expected, you can use whatever sensor you choose (it doesn't have to be temperature or humidity).
automatic pairing
This is how automatic pairing with peers (transmitter (server)/slave boards) works:

- The peer sends a message of typePAIRINGto the server (1) using the broadcast MAC addressff:ff:ff:ff:ff:ff. If you send data to this MAC address, all ESP-NOW devices will receive the message. In order for the server to receive the message, they must be communicating on the same Wi-Fi channel.
- If the peer does not receive a message from the server, it tries to send the same message on a different Wi-Fi channel. He repeats the process until he receives a message from the server.
- The server receives the message and the address of the peer (2).
- The server adds the peer's address to its peer list (3).
- The server replies to the peer with a message of typePAIRINGwith your information (MAC address and channel) (4).
- The partner receives the message and theWiFi.macAdressefrom the server (5).
- The peer adds the address received from the server to its peer list (6).
- The peer tries to send a message to the server address but cannot send*.
- The partner addsWiFi.softAPmacAdressefrom the server to your peer list.
- The peer sends a message to the server.WiFi.softAPmacAdresse.
- The server receives the message from the peer.
- You can now communicate bi-directionally (6).
*ESP32 identicalWIFI_AP_STAThe mode reacts with yourWiFi.macAdressebut oneWiFi.softAPmacAdresseobtained from the companion ESP8266.
WiFi.softAPmacAdressearises fromWiFi.macAdresseAdd 1 to last byte—Check the documentation.
previous requirements
Before proceeding with this project, be sure to review the following prerequisites.
Arduino-IDE
We will program the ESP32 and ESP8266 boards using the Arduino IDE. So before proceeding with this tutorial, make sure you have the ESP32 and ESP8266 boards installed in your Arduino IDE.
- Installation des ESP32-Boards in Arduino IDE (Windows, Mac OS X und Linux)
- Installing the ESP8266 Board in Arduino IDE (Windows, Mac OS X, Linux)
Asynchronous Web Server Libraries
To build the web server you need to install the following libraries:
These libraries cannot be installed via the Arduino Library Manager, so you must copy the library files to the Arduino Installation Libraries folder. Alternatively you can go into your Arduino IDESketch>include library>Add .zip libraryand select the libraries you just downloaded.
Arduino_JSON-Bibliothek
Our examples use theArduinoJSON-Bibliothek von Benoit Blanchonexecution6.18.3. You can install this library in the Arduino IDE Library Manager. just go toSketch>include library>manage librariesand look for the library nameArduinoJSONas follows:

parts needed
To test this project you will need at least three ESP boards. One ESP32 board as server and two ESP sender/slave boards which can be either ESP32 or ESP8266.
ESP32-Server
Here are the server features:
- Automatic pairing with partners (other ESP-NOW boards);
- Receives peer packets;
- Hosts a web server to display recently received packets;
- It also sends data to the other boards (two-way communication with peers).
ESP32-Servercode
Upload the following code to your ESP32 board. This can receive data from several boards. However, the website is only set up to show data from two dashboards. You can easily change the web page to accommodate more boards.
/* Rui Santos Full project details at https://RandomNerdTutorials.com/?s=eng-now Permission is hereby granted, without charge, to any person who obtains a copy of this software and its documentation files. The above copyright notice and this permission notice are included in any copy or substantial portion of the Software. Based on JC Servaye's example: https://github.com/Servayejc/esp_now_web_server/*/#include <esp_now.h>#include <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;// Example structure for receiving data// Must match the structure of the sender typedef struct struct_message { uint8_t msgType; id uint8_t; float temperature; floating buzz; unsigned int readingId;} struct_message;typedef struct struct_pairing { // new structure for pairing uint8_t msgType; id uint8_t; uint8_t macAddr[6]; uint8_t channel;} struct_pairing;struct_message incoming readings;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 PANEL</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:>, html> {font - family: Arial; display: inline block; text-align: center; } p { font-size: 1.2rem; } body { border: 0;} .topnav { overflow: hidden; background-color: #2f4468 ; color: white; font-size: 1 ,7 rem; } .content { padding: 20px; } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .cards { max width: 700px, border: 0 auto, display: grid das; grid spacing d: 2 rem; Grid Template TE columns: repeat(autofit, minmax(300px, 1fr)); } .read { font size: 2.8 rem; } .package {color: #Baby; } .card.temperature { color: #fd7e14; } .card.humidity { color: #1b78e2; } </style></head><body> <div class="topnav"> <h3>ESP-NOW BOARD</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> °C</span></p><p class="packet">Read ID: <span id="rt1"></span></ p> </div> <div class="Card Moisture"> <h4><i class="fas fa-tint"></i> PLATE NO. 1 – MOISTURE</h4><p><span class ="read"> <span id="h1"></span> %</span></p><p class="packet">read 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="read "><span id="t2"></span> °C</span></p><p class="package" >Read ID: <span id="rt2 "></span></p > </div> <div class="Card Moisture"> <h4><i class="fas fa-tint"></i > BOARD #2 - MOISTURE</ h4><p><span class="reading "><span id="h2"></span> %</span></p><p class s=" packet">read 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("Connected Events"); }, 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_reads', function(e) {console.log("new_reads", 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 readDataToSend() { outgoingSetpoints.msgType = DATA;outgoingSetpoints.id = 0;outgoingSetpoints.temp = random(0,40);outgoingSetpoints.hum =random(0,100);outgoingSetpoints.readingId = counter++;}// ------ ----------------------- especially 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 memory set(&slave, 0, size(slave)); const esp_now_peer_info_t *peer = &slave; memcpy(slave.peer_addr, peer_addr, 6); slave.channel = channel; // choose a channel slave.encrypt = 0; // no encryption // check if the peer exists bolexists = 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) { // Pairing successful Serial.println("Pairing successful"); return true; } else { Serial.println("Failed to match"); wrong return; } }} // 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 ? "Successful delivery to ": "Failed delivery to "); printMAC(mac_addr); Serial.println(); } void OnDataRecv(const uint8_t * mac_addr, const uint8_t * incomingData, int len) { Serial.print(len); Serial.print("Data bytes received from: "); printMAC(mac_addr); Serial.println(); StaticJsonDocument<1000> root; chain payload; uint8_t type = incoming data[0]; // the first byte of the message is the message change type (type) { case DATA : // the message is the data type memcpy(&incomingReadings, incomingData, sizeof(incomingReadings)); // Create a JSON document with the received data and send it to the website via event root["id"] = incomingReadings.id; root["temperature"] = incoming readings.temp; root["humidity"] = incomingReadings.hum; root["readingId"] = String(incomingReadings.readingId); serializeJson(root, payload); Serial.print("Send Event:"); serializeJson(root, String); events.send(payload.c_str(), "new_readings", millis()); Serial.println(); interruption; case PAIRING: // the message is a pairing request memcpy(&pairingData, incomingData, sizeof(pairingData)); Serial.println(pairing data. message type); Serial.println(matchData.id); Serial.print("Pairing request for: "); printMAC(mac_addr); Serial.println(); Serial.println(pairing of data.channel); if (pairingData.id > 0) { // do not reproduce on the server itself if (pairingData.msgType == PAIRING) { pairingData.id = 0; // 0 is server // Server is in AP_STA mode: peers must send data to the server. MAC address of the soft AP WiFi.softAPmacAddress(pairingData.macAddr); matchmaking data.channel = chan; Serial.println("Send response"); esp_err_t result = esp_now_send(mac_addr, (uint8_t *) &pairingData, sizeof(pairingData)); addPeer(mac_addr); } } break; } } void initESP_NOW() { // Initialize 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(OnDataRecv); } void setup() { // Initialize Serial Monitor Serial.begin(115200); Serial.println(); Serial.print("Server MAC address: "); Serial.println(WiFi.macAddress()); // Set the device as a station and as a soft access point at the same time WiFi.mode(WIFI_AP_STA); // Set the device as a Wi-Fi station WiFi.begin(ssid, password); while(WiFi.status()!=WL_CONNECTED) { delay(1000); Serial.println("Setting up as a Wi-Fi station..."); } Serial.print( "MAC address of the SOFT AP server: "); Serial.println(WiFi.softAPmacAddress()); chan = WiFi.channel(); Serial.print("IP address of the station: "); 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! The ID of the last message you received is: %u\ n", client -> lastId()); } // send the event with the message "hello!", identify the current millis // and set the reconnection delay to 1 second client->send("hello!", NULL, millis() , 10000); }) ; server.addHandler(&events); // start the server server.begin();}void loop() { static unsigned long lastEventTime = millis(); long unsigned static constant EVENT_INTERVAL_MS = 5000; if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) { events.send("ping",NULL,millis()); lastEventTime = milliseconds(); readDataToSend(); esp_now_send(NULL, (uint8_t *) &outgoingSetpoints, sizeof(outgoingSetpoints)); }}
How the code works
We have already explained in detail how the server code works in aprevious project. So let's only look at the parts that are relevant for the automatic mapping.
message types
The server and transmitters can exchange two types of messages: messages containing pairing data with MAC address, channel and board ID, and messages containing actual data such as sensor readings.
So we create an enumeration type that contains the possible types of incoming messages (PAIRINGjDATA).
enum MessageType {MATCHING, DATA,};
„An enumeration type is a data type (usually user-defined) consisting of a series of named constants called enumerators. The process of creating an enumeration type defines an enumeration. If an identifier, e.g. For example, if a variable is declared with an enumeration type, the variable can be assigned any enumeration type as a value.“. Those: https://playground.arduino.cc/Code/Enum/
After that we create a variable of the type we just created calledtype of message. Remember that this variable can only have two possible values:PAIRINGÖDATA.
message type message type;
data structure
Create a structure that will hold the data we will receive. We call this structurestruct_messageand contains the message type (so we know if we received a message with data or with peer information), board ID, temperature and humidity readings, and the reading ID.
typedef struct struct_message {uint8_t msgType; id uint8_t; float temperature; floating buzz; unsigned int read id;} struct_message;
We also need another structure that holds the pair information to match the pair. We call this structuremating structure. This structure includes the message type, board ID, sender's board MAC address, and Wi-Fi channel.
typedef struct struct_pairing { // neue Struktur zum Paaren uint8_t msgType; id uint8_t; uint8_t macAddr[6]; Kanal uint8_t;} struct_pairing;
We create two variables of typestruct_message, shouted oneincoming readsthis will store the readings coming from the slaves and called anotherprominent target valueswhich contains the data to be sent to the slaves.
struct_message incoming measurements; struct_message outgoingSetpoints;
We also create a variable of typemating structurekeep peer information.
struct_pairing Paarungsdaten;
readDataToSend() function
losread data to be sent ()It should be used to get data from the sensor you are using and place it in the associated frame to send to the slave boards.
void readDataToSend() { outgoingSetpoints.msgType = DATA; SalientSetpoints.id = 0; salient adjustment points.temp = random(0, 40); salientesSetpoints.hum = random(0, 100); outgoingSetpoints.readingId = counter++;}
lostype of messageIt should beDATA. losI WOULDcorresponds to the board id (we set the server board id to 0, the other boards should have an id = 1, 2, 3 etc.). Finally,Temperaturejthe sumPreserve sensor readings. In this case we set them to random values. You need to replace this with the correct functions to get data from your sensor. Each time we send a new set of readings, we increase themcountertopVariable.
Add a partner
We create a function calledaddPeer()which returns a boolean variable (eitherrealÖNOT CORRECT), which shows whether the pairing process was successful or not. This function attempts to add pairs. It will be called later when the board receives a message of the typePAIRING. If the pair is already in the pairs list, it is returnedreal. also returnsrealwhen the pair is added successfully. come backNOT CORRECT, if not, you can add the couple to the list.
bool addPeer(const uint8_t *peer_addr) { // add peering memset(&slave, 0, sizeof(slave)); const esp_now_peer_info_t *peer = &slave; memcpy(slave.peer_addr, peer_addr, 6); slave.channel = channel; // choose a channel slave.encrypt = 0; // no encryption // check if the peer exists bolexists = 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) { // Pairing successful Serial.println("Pairing successful"); return true; } else { Serial.println("Failed to match"); wrong return; } }}
Receiving and processing ESP-NOW messages
losOnDataRecv()The function runs when it receives a new ESP-NOW packet.
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {
In this function, output the length of the message and the MAC address of the sender:
Serial.print(len);Serial.print("Data bytes received from:");printMAC(mac_addr);
Earlier we saw that we can receive two types of messages:PAIRINGjDATA. Therefore, we need to handle the message content differently depending on the message type. We can get the message type as follows:
uint8_t type = incoming data[0]; // The first byte of the message is the message type
Then we execute different code depending on whether the message is of typeDATAÖPAIRING.
if it's the typeDATA, copy the information into theincoming datavariable in theincoming readsstructure variable.
memcpy(&incoming reads, incoming data, size of (incoming reads));
Then create a JSON document with the obtained information (root):
// create a JSON document with the received data and send it to the website via event root["id"] = incomingMeasuringValues.id;root["temperature"] = incomingMeasuringValues.temp;root["humidity"] = incomingMeasuringValues.hum ; root["readingId"] = String(incomingReadings.readingId);
Convert the JSON document to a string (Useful charge):
serializeJson(root, payload);
After collecting all the data received about theUseful chargevariable, it sends this information as an event to the browser ("new_readings").
events.send(payload.c_str(), "nuevas_lecturas", millis());
We've seenin a previous projecthow these events are handled on the client side.
If the message is of typePAIRING, contains the couple's information.
case PAIRING: // The message is a pairing request
We store the data received in theincoming datavariable 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 for: ");printMAC(mac_addr);Serial. println(); Serial.println(pairing of data.channel);
The server replies with its MAC address (in AP mode) and channel, so the peer knows it sent the information over the correct channel and can add the server as a peer.
if (pairingData.id > 0) { // do not reproduce on the server itself if (pairingData.msgType == PAIRING) { pairingData.id = 0; // 0 is server // Server is in AP_STA mode: peers must send data to the server. MAC address of the soft AP WiFi.softAPmacAddress(pairingData.macAddr); matchmaking data.channel = chan; Serial.println("Send response"); esp_err_t result = esp_now_send(mac_addr, (uint8_t *) &pairingData, sizeof(pairingData));
Finally, the server adds the sender to its peer list usingaddPeer()function that we created earlier.
addPeer(mac_addr);
Initialize ESP NOW
losinitESP_NOW()The function initializes ESP-NOW and registers the callback functions for sending and receiving data.
Void initESP_NOW () {// ESP-NOW initialisieren if (esp_now_init () ! = ESP_OK) {Serial.println ( "Fehler beim Initialisieren von ESP-NOW"); Devolver; } esp_now_register_send_cb (OnDataSent); esp_now_register_recv_cb(OnDataRecv);}
Attitude()
In themAttitude(), print the MAC address of the board:
Serial.println(WiFi.macAddress());
Configure the ESP32 receiver as a station and as a soft access point at the same time:
WiFi.mode (WIFI_AP_STA);
The following lines connect the ESP32 to your local network and output the IP address and WiFi channel:
WiFi.begin(ssid, password); while(WiFi.status()!=WL_CONNECTED) { delay(1000); Serial.println("Setting up as a Wi-Fi station..."); }
Print the card's MAC address in access point mode, which is different from the MAC address in station mode.
Serial.print("Servidor SOFT AP MAC-Adresse: ");Serial.println(WiFi.softAPmacAddress());
Get the wifi channel from the board and print it to the serial monitor.
chan = WiFi.channel();Serial.print("IP-Adresse der Station: ");Serial.println(WiFi.localIP());Serial.print("Wi-Fi-Kanal: ");Serial.println( wifi. Kanal());
Initialize ESP-NOW by callinginitESP_NOW()function that we created earlier.
initESP_NOW();
Send data messages to sender boards
In themKreis(), every 5 seconds (EVENT_INTERVAL_MS) Retrieve data from a sensor or sample data by calling theread data to be sent ()Function. Add new dataprominent target valuesStructure.
LearnDataParaEnviar();
Finally, send this data to all registered peers.
esp_now_send(NULL, (uint8_t *) &outgoingSetpoints, sizeof(outgoingSetpoints));
Here's how the server code works when it comes to processing ESP-NOW messages and adding peers automatically.
Servertest
After uploading the code to the receiving board, press the built-in EN/RST button. The ESP32 IP address should be printed on the serial monitor, as should the Wi-Fi channel.

You can access the web server using the whiteboard's IP address. No data is displayed at the moment because we have not yet prepared the sender panels. Let the server board run the code.
ESP32/ESP8266-Sender
Here are the characteristics of the sender board:
- Automatic pairing with the server;
- send packets with sensor readings to the server;
- It also receives data from the server (two-way communication).
automatic pairing
This is how automatic pairing with the server works:
- The sender does not have access to the router;
- The sender does not know the MAC address of the server;
- The server must be running for this to work (with the code above);
- The transmitter now sets esp to channel 1;
- The server adds an entry with the broadcast address to its peer list;
- The sender sends aPAIRINGMessage request in broadcast mode:
- If the server gets the message, we're on the right channel:
- The server adds the received MAC to its peer list (previous section);
- The server responds to the MAC address with a message containing its channel number and MAC address (previous section);
- The sender replaces the broadcast address with the server address in its peer list.
- Plus
- The transmitter repeats the process on the next channel.
WiFi.softAPmacAdressearises fromWiFi.macAdresseAdd 1 to last byte—Check the documentation.
ESP32 sender code
Upload the following code to your ESP32 board.
/* Rui Santos Full project details at https://RandomNerdTutorials.com/?s=eng-now Permission is hereby granted, without charge, to any person who obtains a copy of this software and its documentation files. The above copyright notice and this permission notice are included in any copy or substantial portion of the Software. Based on JC Servaye's 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 11 // for North America // 13 to Europauint8_t serverAddress[] = {0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF };/ /Structure for sending data//Must match receiver structure// Sample structure for receiving data//Must match sender structure typedef struct struct_message { uint8_t msgType; id uint8_t; float temperature; floating buzz; unsigned int readingId;} struct_message;typedef struct struct_pairing { // new structure for pairing uint8_t msgType; id uint8_t; uint8_t macAddr[6]; channel uint8_t;} struct_pairing;//Create 2 struct_message struct_message myData; // data to sendstruct_message inData; // received data struct_pairing pairingData;enum PairingStatus {NOT_PARIRED, PAIR_REQUEST, PAIR_REQUESTED, PAIR_PAIRED,};PairingStatus pairingStatus = NOT_PAIRED;enum MessageType {PAIRING, DATA,};MessageType messageType;#ifdef SAVE_CHANNEL int lastifChannel1; // simulate temperature and humidity data float t = 0; float h = 0; unsigned long currentMillis = millis (); unsigned long previousMillis = 0; // Stores the last time the temperature was published const long interval = 10000; // interval at which sensor values should be posted unsigned long start; // Used to time unsigned matchmaking int readId = 0; //simulate floating temperature reading readDHTTemperature() { t = random(0.40); return t;} //simulate floating humidity reading readDHTHumidity() { h = random(0,100); return h;}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(eng_now_peer_info_t)); partner.channel = chan; par.encrypt = false; memcpy(peer.peer_addr, mac_addr, sizeof(uint8_t[6])); If (esp_now_add_peer(&peer) != ESP_OK){Serial.println("Error adding 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\nSend status of last packet:\t"); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Successful delivery" : "Failed delivery"); } void OnDataRecv (const uint8_t * mac_addr, const uint8_t * incomingData, int len) { Serial.print( "Packet received from:"); printMAC(mac_addr); Serial.println(); Serial.print("datasize="); Serial.println(size(incoming data)); uint8_t type = incoming data[0]; switch (type) { case DATA : // receive data from server memcpy(&inData, incomingData, sizeof(inData)); Serial.print("ID="); Serial.println(inData.id); Serial.print("set temperature="); Serial.println(inData.temp); Serial.print("humidity setpoint="); Serial.println(inData.hum); Serial.print("read id="); Serial.println(inData.readingId); if(inData.readingId %2 == 1) { digitalWrite(LED_BUILTIN, LOW); } Else { DigitalWrite(LED_BUILTIN, HIGH); } break; case PAIRING: // Receive pairing data from server memcpy(&pairingData, incomingData, sizeof(pairingData)); if (pairingData.id == 0) { // the message comes from the server printMAC(mac_addr); Serial.print("Matching done for"); printMAC(matchData.macAddr); Serial.print("on channel"); Serial.print(Pairing data.channel); // channel used by the server Serial.print(" in "); Serial.print(millis()-start); Serial.println("ms"); addPeer (peering data.macAddr, pairing data.channel); // add the server to the pairing list #ifdef SAVE_CHANNEL lastChannel = pairingData.channel; EEPROM.write(0, pairing data.channel); EEPROM.commit(); #endif pairing status = PAIR_PARIRED; // sets 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 up callback routines esp_now_register_send_cb(OnDataSent); esp_now_register_recv_cb(OnDataRecv); // configure pairing data to be sent to the server pairingData.msgType = PAIRING; pairingData.id = BOARD_ID; pairing data.channel = channel; // add peer and send request addPeer(serverAddress, channel); esp_now_send(server address, (uint8_t *) & matchdata, size of (matchdata)); previousMillis = millis(); matchmaking status = MATCHING_REQUESTED; interruption; case PAIR_REQUESTED: // Timeout to get a response from the server currentMillis = millis(); if(current millis - previous millis > 250) { previous millis = current millis; // timeout expired, try next channel channel ++; if (channel > MAX_CHANNEL) { channel = 1; } pairing status = PAIR_REQUEST; } break; case PAIR_PARIRED: // nothing to do here break; } return PairingStatus; } void setup() { Serial.begin(115200); Serial.println(); PinMode(LED_BUILTIN, OUTPUT); Serial.print("MAC address of client board: "); Serial.println(WiFi.macAddress()); WiFi.mode (WIFI_STA); WiFi.disconnect(); start = milliseconds(); #ifdef SAVE_CHANNEL EEPROM.begin(10); last channel = EEPROM.read(0); Serial.println(last channel); if (lastchannel >= 1 && lastchannel <= MAX_CHANNEL) { channel = lastchannel; } Serial.println(channel); #endif pairing status = PAIR_REQUEST;} void loop() { if (autoPairing() == PAIR_PAIRED) { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { // Store the last time a new reading was posted previousMillis = currentMillis; //set values to send myData.msgType = DATA; myData.id = BOARD_ID; myData.temp = readDHTTemperature(); myData.hum = readDTHTHumidity(); myData.readingId = readId++; esp_err_t result = esp_now_send(serverAddress, (uint8_t *) &myData, sizeof(myData)); } }}
Sender code ESP8266
If you are using ESP8266 cards, use the following code instead. It is similar to the code above but uses the ESP8266 specific ESP-NOW functions.
/* Rui Santos Full project details at https://RandomNerdTutorials.com/?s=eng-now Permission is hereby granted, without charge, to any person who obtains a copy of this software and its documentation files. The above copyright notice and this permission notice are included in any copy or substantial portion of the Software. Based on JC Servaye's example: https://github.com/Servayejc/esp8266_espnow*/#include <ESP8266WiFi.h>#include <espnow.h>uint8_t channel = 1;int readingId = 0;int id = 2; unsigned long currentMillis = millis(); unsigned long lastTime = 0; unsigned long timerDelay = 2000; // send reads timeruint8_t broadcastAddressX[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};enum PairingStatus {PAIR_REQUEST, PAIR_REQUESTED, PAIR_PAIRED, };PairingStatus pairingStatus = PAIR_REQUEST;enum MessageType {PAIRING, DATAMessageTypeMessageType,}; ;/ / Define variables to store the DHT readings sent to the float temp. Float IncomingHum;// Define variables to store incoming metrics Float IncomingTemp; Float IncomingHum;Int IncomingReadingsId;// Update DHT readings every 10 seconds //Const Long Interval = 10000; unsigned previous longMillis = 0; // saves the last DHT update //example structure for sending data//must match structure type of receiver struct struct_message { uint8_t msgType; id uint8_t; float temperature; floating buzz; unsigned int readingId;} struct_message;typedef struct struct_pairing { // new structure for pairing uint8_t msgType; id uint8_t; uint8_t macAddr[6]; uint8_t channel;} struct_pairing;// Generate a struct_message named myDatastruct_message myData;struct_message incomingReadings;struct_pairing pairingData;#define BOARD_ID 2unsigned long start;// Callback when sending data void OnDataSent(uint8_t *mac_addr, uint8_t" SendStaturial send Send status of the last packet: "); if (sendStatus == 0) { Serial.println("Successful delivery"); } else { Serial.println("Delivery failed"); }}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 on serial monitor Serial.println("INCOMING READINGS"); Serial.print("Temperature: "); Serial.print(incomingTemp); Serial.println("ºC"); Serial.print("Moisture: "); 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("Message size: "); Serial.print(long); Serial.print("from"); printMAC(mac); Serial.println(); uint8_t type = incoming data[0]; switch (type) { case DATA : memcpy(&incoming reads, incoming data, size of (incoming reads)); Serial.print(long); Serial.print("bytes of data received from: "); printMAC(mac); Serial.println(); incomingTemp = incomingReadings.temp; incomingHum = incomingReadings.hum; printIncomingReadings(); if(incoming reads.readingId %2 == 1) { digitalWrite(LED_BUILTIN, LOW); } Else { DigitalWrite(LED_BUILTIN, HIGH); } break; case PAIRING: memcpy(&pairingData, incomingData, sizeof(pairingData)); if (pairingData.id == 0) { // message comes from server Serial.print ("Pairing done for"); printMAC(matchData.macAddr); Serial.print("on channel"); Serial.print(Pairing data.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 pair list pairingStatus = PAIR_PAIRED; // sets the pairing status } break; } } void getReadings() { // read temperature temperature = 22.5; humidity = 55.5; } PairingStatus autoPairing () { switch (pairingStatus) { case PAIR_REQUEST: Serial.print("channel pairing request"); Serial.println(channel); // delete 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(series); WiFi.disconnect(); // Initialize ESP-NOW if (esp_now_init() != 0) {Serial.println("Error initializing ESP-NOW"); } esp_now_set_self_role(ESP_NOW_ROLE_COMBO); // Set up callback routines esp_now_register_send_cb(OnDataSent); esp_now_register_recv_cb(OnDataRecv); // sets the pairing data to be sent to the server pairingData.id = BOARD_ID; pairing data.channel = channel; previousMillis = millis(); //add right away and send request Serial.println(esp_now_send(broadcastAddressX,(uint8_t*) & pairingData, sizeof(pairingData))); matchmaking status = MATCHING_REQUESTED; interruption; case PAIR_REQUESTED: // Timeout to get a response from the server currentMillis = millis(); if(current millis - previous millis > 100) { previous millis = current millis; // timeout expired, try next channel channel ++; if (channel > 11) { channel = 0; } pairing status = PAIR_REQUEST; } break; case PAIR_PARIRED: //Serial.println("Paired!"); interruption; } return pairingStatus; } void setup() { // Init Serial Monitor Serial.begin(74880); PinMode(LED_BUILTIN, OUTPUT); //Initialize DHT sensor //Set device as Wi-Fi station WiFi.mode(WIFI_STA); Serial.println(WiFi.macAddress()); WiFi.disconnect(); // Initialize 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 to 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(); long unsigned static constant EVENT_INTERVAL_MS = 10000; if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) { Serial.print(".."); getReads(); //set values to send myData.msgType = DATA; mydata.id = 2; myData.temp = temperature; myData.hum = humidity; myData.readingId = readId++; // Send message to all peers via ESP-NOW esp_now_send(pairingData.macAddr, (uint8_t *) &myData, sizeof(myData)); lastEventTime = milliseconds(); } }}
How the code works
ESP32 and ESP8266 differ slightly when it comes to ESP-NOW specific features. But they have a similar structure. So let's just look at the ESP32 code.
We'll take a look at the relevant sections dealing with automatic synchronization with the server. The rest of the code has already been explained in detail in aprevious project.
Set the Board ID
Define the sender's Board ID. Each board must have a different ID so the server knows who sent the message. Board ID 0 is reserved for the server, so you should start numbering your sender boards from 1.
// Konfiguriere su ID de tablero (ESP32 Remitente #1 = BOARD_ID 1, ESP32 Remitente #2 = BOARD_ID 2, etc.) #define BOARD_ID 1
Define the maximum number of channels
The sender goes through different Wi-Fi channels until it finds the server. Therefore set the maximum number of channels.
#define MAX_CHANNEL 11 // for North America // 13 in Europe
MAC-Address of Servers
The sender board does not know the MAC address of the server. So we start by sending a message to broadcast MAC address FF:FF:FF:FF:FF:FF on different channels. If we send messages to this MAC address, all ESP-NOW devices will receive this message. The server then responds with your actual MAC address when we have found the correct WiFi channel.
Serveradresse uint8_t[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
data structure
Similar to the server code, we create two structures. One to get actual data and one to get details for the matchup.
//Structure for sending data//Must match receiver structure// Sample structure for receiving data//Must match sender structure typedef struct struct_message { uint8_t msgType; id uint8_t; float temperature; floating buzz; unsigned int readingId;} struct_message;typedef struct struct_pairing { // new structure for pairing uint8_t msgType; id uint8_t; uint8_t macAddr[6]; channel uint8_t;} struct_pairing;//Create 2 struct_message struct_message myData; // data to sendstruct_message inData; // received data struct_pairing pairingData;
statue mating
Next we create an enumeration type calledParingStatuswhich can have the following values:NOT PAIRED,PAIR_REQUEST,PAIR_REQUESTED, jPAIR_PAIRED. This will help us track the matchmaking status situation.
enum PairingStatus {NOT_PARIRED, PAIR_REQUEST, PAIR_REQUESTED, PAIR_PAIRED,};
We create a variable of this type calledpairing status. When the board first boots up, it's not paired, so it's setNOT PAIRED.
Pairing status Pairing status = NOT_PARIRED;
message types
As we did on the server, we also have onetype of messageso we know if we received a pairing message or a message with data.
enum message type {PAIRING, DATA,}; message type message type;
Add a partner
This function adds a new pair to the list. It accepts the peer's MAC address and channel as arguments.
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)); compañero.canal = chan; par.cifrar = falso; memcpy(peer.peer_addr, mac_addr, sizeof(uint8_t[6])); If (esp_now_add_peer(&peer) != ESP_OK){Serial.println("Error al agregar peer"); Devolver; } memcpy(direction del server, mac_addr, sizeof(uint8_t[6]));}
Receiving and processing ESP-NOW messages
losOnDataRecv()The function runs when it receives a new ESP-NOW packet.
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *incomingData, int len) {
In this function, output the length of the message and the MAC address of the sender:
Serial.print("Packet received from: ");printMAC(mac_addr);Serial.println();Serial.print("data size = ");Serial.println(size(incoming data));
Earlier we saw that we can receive two types of messages:PAIRINGjDATA. Therefore, we need to handle the message content differently depending on the message type. We can get the message type as follows:
uint8_t type = incoming data[0]; // The first byte of the message is the message type
Then we execute different code depending on whether the message is of typeDATAÖPAIRING.
if it's the typeDATA, copy the information into theincoming datavariable in theinstantlystructure variable.
memcpy(&inData, incomingData, sizeof(inData));
Then we simply print out the received data on the Serial Monitor. You can perform any other task that may be useful for your project with the received data.
Serial.print("ID = ");Serial.println(inData.id);Serial.print("Setpoint temp = ");Serial.println(inData.temp);Serial.print("SetPoint de humedad = ") ; Serial.println (inData.hum); Serial.print ( "Lese-ID = "); Serial.println (inData.readingId);
In this case, we blink the built-in LED as long as the read ID is an odd number, but it can do any other task depending on the data received.
if (incomingReadings.readingId % 2 == 1) {digitalWrite (LED_BUILTIN, LOW);} else {digitalWrite (LED_BUILTIN, HIGH);}break;
If the message is of typePAIRINGlet's first check if the received message is from the server and not from another sending board. We know that because theI WOULDvariable for the server0.
case PAIRING: // Receive pairing data from server memcpy(&pairingData, incomingData, sizeof(pairingData)); if (pairingData.id == 0) { // the message comes from the server
Next we print the MAC address and channel. This information is sent by the server.
Serial.print("Pairing complete for");printMAC(pairingData.macAddr);Serial.print("on channel");Serial.print(pairingData.channel); // Channel used by the server
Now that we know the details of the server, we can make a calladdPeer()and pass the server's MAC address and channel as arguments to add the server to the peer list.
addPeer (peering data.macAddr, pairing data.channel); // add the server to the peer list
If the pairing is successful, we change thepairing statusaPAIR_PAIRED.
PairingState = PAIR_PARIRED; // Set pairing status
automatic pairing
losAuto-Match()Function returns the pairing status.
status auto match() {
We can have different scenarios. if it's the typePAIR_REQUEST, it configures the ESP-NOW callback functions and sends the first message of the typePAIRINGto the broadcast address on a predefined channel (starting at 1). After that we change the matchmaking status toPAIR_REQUESTED(means we have already made a request).
PAIR_REQUEST case: Serial.print("Channel pairing request"); Serial number callback esp_now_register_send_cb(OnDataSent); esp_now_register_recv_cb(OnDataRecv); // configure pairing data to be sent to the server pairingData.msgType = PAIRING;pairingData.id = BOARD_ID; pairing data.channel = channel; // add peer and send request add peer(server address, channel); esp_now_send(server address, (uint8_t *) & matchdata, size of (matchdata)); previousMillis = millis(); pairing status = PAIR_REQUESTED;
After sending a matchmaking message, we wait a while to see if we get a message from the server. If not, we try the next WiFi channel and change itpairing statusaPAIR_REQUESTagain to have the board send a new request on a different Wi-Fi channel.
case PAIR_REQUESTED:// Timeout to get a response from the server currentMillis = millis();if(currentMillis - previousMillis > 250) { previousMillis = currentMillis; // timeout expired, try next channel channel ++; if (channel > MAX_CHANNEL) { channel = 1; } pairing status = PAIR_REQUEST;}break;
If hepairing statusesPAIR_PAIRED, which means that we're already paired with the server, we don't have to do anything.
case PAIR_PARIRED: // nothing to do here break;
Finally, return thosepairing status.
return the pairing status;
Attitude()
In themAttitude(), choosepairing statusaPAIR_REQUEST.
PairingState = PAIR_REQUEST;
Kreis()
In themKreis(), verify that the board is paired with the server before doing anything else.
if (automatch() == MATCH_MATCHED) {
This is runningAuto-Match()work and handle automatic pairing with the server. When the board is paired with the transmitter (PAIR_PAIRED), we can communicate with the server to exchange data with messages of typeDATA.
Sending messages to the server
In this case we send any temperature and humidity values, but you can also exchange any other data with the server.
unsigned long currentMillis = millis();if (currentMillis - previousMillis >= interval) { // store the last time a new reading was posted previousMillis = currentMillis; //set values to send myData.msgType = DATA; myData.id = BOARD_ID; myData.temp = readDHTTemperature(); myData.hum = readDTHTHumidity(); myData.readingId = readId++; esp_err_t result = esp_now_send(server address, (uint8_t *) &myData, sizeof(myData));}
Sender's number plate test
Now you can test the emitter boards. We recommend opening a serial communication with the server in another software such as Putty, so you can see what's happening on the server and the sender at the same time.
After getting the server up and running, you can upload the sender code to the other boards.
After uploading the code, open the Serial Monitor with a baud rate of 115200 and press the RST button to make the board start executing the code.
The sender must return this.

As you can see, it first sends a matchmaking request through different channels until it gets a response from the server. In this case use channel 6.
After that, we receive messages from the server. We also send messages to the server.
The following happens on the server side:

The server receives a pairing request from the sender. It will be matched with the sender. In my case it was already paired because I ran this code before. After the data, we start sending and receiving data.
You can upload the sender code to multiple boards and they will all be automatically paired with the server. The emitter boards can be ESP32 or ESP8266 boards. Make sure you are using the correct code for the board you are using.
Now you can go to the IP address of the server to see the sender's dashboard metrics that are displayed on the dashboard. The web page is set up to display readings from two panels. If you want to show more metrics, you need to change the webpage.

End
In this tutorial, we will show you how to create an ESP-NOW web server, automatically match it with peers, and establish two-way communication between the server and the sender boards.
You can customize the parts of the code that deal with automatic pairing and use them in your ESP-NOW samples.
we would like to thank youJean-Claude Servayefor sharing your sketches of the ESP-NOW Auto Pairing Code with us. We just made some changes to the sketches. The original codes can be found athis GitHub page.
You may also like...
- More examples from ESP-NOW…
- Learn ESP32 with the Arduino IDE eBook
- Home Automation with ESP8266 Ebook
- Build a web server with ESP32 and ESP8266 eBook
Thank you for reading.