Learn how to use ESP-MESH networking protocol to build a mesh network with the ESP32 and ESP8266 NodeMCU boards. ESP-MESH allows multiple devices (nodes) to communicate with each other under a single wireless local area network. It is supported on the ESP32 and ESP8266 boards. In this guide, we’ll show you how to get started with ESP-MESH using the Arduino core.

This article covers the following topics:
If you want to program the ESP32 and ESP8266 boards using Arduino IDE, you should have the ESP32 or ESP8266 add-ons installed. Follow the next guides:
If you want to program the ESP32/ESP8266 using VS Code + PlatformIO, follow the next tutorial:
Accordingly to the Espressif documentation:
“ESP-MESH is a networking protocol built atop the Wi-Fi protocol. ESP-MESH allows numerous devices (referred to as nodes) spread over a large physical area (both indoors and outdoors) to be interconnected under a single WLAN (Wireless Local-Area Network).
ESP-MESH is self-organizing and self-healing meaning the network can be built and maintained autonomously.” For more information, visit the ESP-MESH official documentation.
In a traditional Wi-Fi network architecture, a single node (access point – usually the router) is connected to all other nodes (stations). Each node can communicate with each other using the access point. However, this is limited to the access point wi-fi coverage. Every station must be in the range to connect directly to the access point. This doesn’t happen with ESP-MESH.

With ESP-MESH, the nodes don’t need to connect to a central node. Nodes are responsible for relaying each others transmissions. This allows multiple devices to spread over a large physical area. The Nodes can self-organize and dynamically talk to each other to ensure that the packet reaches its final node destination. If any node is removed from the network, it is able to self-organize to make sure that the packets reach their destination.

The painlessMesh library allows us to create a mesh network with the ESP8266 or/and ESP32 boards in an easy way.
“painlessMesh is a true ad-hoc network, meaning that no-planning, central controller, or router is required. Any system of 1 or more nodes will self-organize into fully functional mesh. The maximum size of the mesh is limited (we think) by the amount of memory in the heap that can be allocated to the sub-connections buffer and so should be really quite high.” More information about the painlessMesh library.
You can install painlessMesh through the Arduino Library manager. Go to Tools > Manage Libraries. The Library Manager should open.
Search for “painlessmesh” and install the library. We’re using Version 1.4.5

This library needs some other library dependencies. A new window should pop up asking you to install any missing dependencies. Select “Install all”.

If this window doesn’t show up, you’ll need to install the following library dependencies:
If you’re using PlatformIO, add the following lines to the platformio.ini file to add the libraries and change the monitor speed.
For the ESP32:
monitor_speed = 115200 lib_deps = painlessmesh/painlessMesh @ ^1.4.5 ArduinoJson arduinoUnity TaskScheduler AsyncTCP
For the ESP8266:
monitor_speed = 115200 lib_deps = painlessmesh/painlessMesh @ ^1.4.5 ArduinoJson TaskScheduler ESPAsyncTCP
To get started with ESP-MESH, we’ll first experiment with the library’s basic example. This example creates a mesh network in which all boards broadcast messages to all the other boards.
We’ve experimented this example with four boards (two ESP32 and two ESP8266). You can add or remove boards. The code is compatible with both the ESP32 and ESP8266 boards.

Copy the following code to your Arduino IDE (code from the library examples). The code is compatible with both the ESP32 and ESP8266 boards.
If you’re using an ESP32, you need to downgrade your ESP32 boards’ add-on to version 2.0.X. At the moment, the painlessMesh library is not compatible with version 3.X.
/* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-mesh-esp32-esp8266-painlessmesh/ This is a simple example that uses the painlessMesh library: https://github.com/gmag11/painlessMesh/blob/master/examples/basic/basic.ino */ #include "painlessMesh.h" #define MESH_PREFIX "whateverYouLike" #define MESH_PASSWORD "somethingSneaky" #define MESH_PORT 5555 Scheduler userScheduler; // to control your personal task painlessMesh mesh; // User stub void sendMessage() ; // Prototype so PlatformIO doesn't complain Task taskSendMessage( TASK_SECOND * 1 , TASK_FOREVER, &sendMessage ); void sendMessage() { String msg = "Hi from node1"; msg += mesh.getNodeId(); mesh.sendBroadcast( msg ); taskSendMessage.setInterval( random( TASK_SECOND * 1, TASK_SECOND * 5 )); } // Needed for painless library void receivedCallback( uint32_t from, String &msg ) { Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str()); } void newConnectionCallback(uint32_t nodeId) { Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId); } void changedConnectionCallback() { Serial.printf("Changed connections\n"); } void nodeTimeAdjustedCallback(int32_t offset) { Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset); } void setup() { Serial.begin(115200); //mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT ); mesh.onReceive(&receivedCallback); mesh.onNewConnection(&newConnectionCallback); mesh.onChangedConnections(&changedConnectionCallback); mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback); userScheduler.addTask( taskSendMessage ); taskSendMessage.enable(); } void loop() { // it will run the user scheduler as well mesh.update(); }
Before uploading the code, you can set up the MESH_PREFIX (it’s like the name of the MESH network) and the MESH_PASSWORD variables (you can set it to whatever you like).
Then, we recommend that you change the following line for each board to easily identify the node that sent the message. For example, for node 1, change the message as follows:
String msg = "Hi from node 1 ";
Start by including the painlessMesh library.
#include "painlessMesh.h"Then, add the mesh details. The MESH_PREFIX refers to the name of the mesh. You can change it to whatever you like.
#define MESH_PREFIX "whateverYouLike"The MESH_PASSWORD, as the name suggests is the mesh password. You can change it to whatever you like.
#define MESH_PASSWORD "somethingSneaky"All nodes in the mesh should use the same MESH_PREFIX and MESH_PASSWORD.
The MESH_PORT refers to the the TCP port that you want the mesh server to run on. The default is 5555.
#define MESH_PORT 5555It is recommended to avoid using delay() in the mesh network code. To maintain the mesh, some tasks need to be performed in the background. Using delay() will stop these tasks from happening and can cause the mesh to lose stability/fall apart.
Instead, it is recommended to use TaskScheduler to run your tasks which is used in painlessMesh itself.
The following line creates a new Scheduler called userScheduler.
Scheduler userScheduler; // to control your personal task
Create a painlessMesh object called mesh to handle the mesh network.
Create a task called taskSendMessage responsible for calling the sendMessage() function every second as long as the program is running.
Task taskSendMessage(TASK_SECOND * 1 , TASK_FOREVER, &sendMessage);
The sendMessage() function sends a message to all nodes in the message network (broadcast).
void sendMessage() { String msg = "Hi from node 1"; msg += mesh.getNodeId(); mesh.sendBroadcast( msg ); taskSendMessage.setInterval(random(TASK_SECOND * 1, TASK_SECOND * 5)); }
The message contains the “Hi from node 1” text followed by the board chip ID.
String msg = "Hi from node 1"; msg += mesh.getNodeId();
To broadcast a message, simply use the sendBroadcast() method on the mesh object and pass as argument the message (msg) you want to send.
mesh.sendBroadcast(msg);
Every time a new message is sent, the code changes the interval between messages (one to five seconds).
taskSendMessage.setInterval(random(TASK_SECOND * 1, TASK_SECOND * 5));
Next, several callback functions are created that will be called when specific events happen on the mesh.
The receivedCallback() function prints the message sender (from) and the content of the message (msg.c_str()).
void receivedCallback( uint32_t from, String &msg ) { Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str()); }
The newConnectionCallback() function runs whenever a new node joins the network. This function simply prints the chip ID of the new node. You can modify the function to do any other task.
void newConnectionCallback(uint32_t nodeId) { Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId); }
The changedConnectionCallback() function runs whenever a connection changes on the network (when a node joins or leaves the network).
void changedConnectionCallback() { Serial.printf("Changed connections\n"); }
The nodeTimeAdjustedCallback() function runs when the network adjusts the time, so that all nodes are synchronized. It prints the offset.
void nodeTimeAdjustedCallback(int32_t offset) { Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset); }
In the setup(), initialize the serial monitor.
void setup() { Serial.begin(115200);
Choose the desired debug message types:
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
Initialize the mesh with the details defined earlier.
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
Assign all the callback functions to their corresponding events.
mesh.onReceive(&receivedCallback); mesh.onNewConnection(&newConnectionCallback); mesh.onChangedConnections(&changedConnectionCallback); mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
Finally, add the taskSendMessage function to the userScheduler. The scheduler is responsible for handling and running the tasks at the right time.
userScheduler.addTask(taskSendMessage);
Finally, enable the taskSendMessage, so that the program starts sending the messages to the mesh.
taskSendMessage.enable();
To keep the mesh running, add mesh.update() to the loop().
void loop() { // it will run the user scheduler as well mesh.update(); }
Upload the code provided to all your boards. Don’t forget to modify the message to easily identify the sender node
With the boards connected to your computer, open a serial connection with each board. You can use the Serial Monitor, or you can use a software like PuTTY and open multiple windows for all the boards.
You should see that all boards receive each others messages. For example, these are the messages received by Node 1. It receives the messages from Node 2, 3 and 4.

You should also see other messages when there are changes on the mesh: when a board leaves or joins the network.

In this next example, we’ll exchange sensor readings between 4 boards (you can use a different number of boards). Every board receives the other boards’ readings.

As an example, we’ll exchange sensor readings from a BME280 sensor, but you can use any other sensor.
Here’s the parts required for this example:
In this example, we’ll exchange the sensor readings in JSON format. To make it easier to handle JSON variables, we’ll use the Arduino_JSON library.
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 as follows:

If you’re using VS Code with PlatformIO, include the libraries in the platformio.ini file as follows:
ESP32
monitor_speed = 115200 lib_deps = painlessmesh/painlessMesh @ ^1.4.5 ArduinoJson arduinoUnity AsyncTCP TaskScheduler adafruit/Adafruit Unified Sensor @ ^1.1.4 adafruit/Adafruit BME280 Library @ ^2.1.2 arduino-libraries/Arduino_JSON @ ^0.1.0
ESP8266
monitor_speed = 115200 lib_deps = painlessmesh/painlessMesh @ ^1.4.5 ArduinoJson TaskScheduler ESPAsyncTCP adafruit/Adafruit Unified Sensor @ ^1.1.4 adafruit/Adafruit BME280 Library @ ^2.1.2 arduino-libraries/Arduino_JSON @ ^0.1.0
Wire the BME280 sensor to the ESP32 or ESP8266 default I2C pins as shown in the following schematic diagrams.

Recommended reading: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity)

Recommended reading: ESP8266 with BME280 using Arduino IDE (Pressure, Temperature, Humidity)
Upload the following code to each of your boards. This code reads and broadcasts the current temperature, humidity and pressure readings to all boards on the mesh network. The readings are sent as a JSON string that also contains the node number to identify the sender board.
/* Rui Santos Complete project details at https://RandomNerdTutorials.com/esp-mesh-esp32-esp8266-painlessmesh/ 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. */ #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> #include "painlessMesh.h" #include <Arduino_JSON.h> // MESH Details #define MESH_PREFIX "RNTMESH" //name for your MESH #define MESH_PASSWORD "MESHpassword" //password for your MESH #define MESH_PORT 5555 //default port //BME object on the default I2C pins Adafruit_BME280 bme; //Number for this node int nodeNumber = 2; //String to send to other nodes with sensor readings String readings; Scheduler userScheduler; // to control your personal task painlessMesh mesh; // User stub void sendMessage() ; // Prototype so PlatformIO doesn't complain String getReadings(); // Prototype for sending sensor readings //Create tasks: to send messages and get readings; Task taskSendMessage(TASK_SECOND * 5 , TASK_FOREVER, &sendMessage); String getReadings () { JSONVar jsonReadings; jsonReadings["node"] = nodeNumber; jsonReadings["temp"] = bme.readTemperature(); jsonReadings["hum"] = bme.readHumidity(); jsonReadings["pres"] = bme.readPressure()/100.0F; readings = JSON.stringify(jsonReadings); return readings; } void sendMessage () { String msg = getReadings(); mesh.sendBroadcast(msg); } //Init BME280 void initBME(){ if (!bme.begin(0x76)) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } } // Needed for painless library void receivedCallback( uint32_t from, String &msg ) { Serial.printf("Received from %u msg=%s\n", from, msg.c_str()); JSONVar myObject = JSON.parse(msg.c_str()); int node = myObject["node"]; double temp = myObject["temp"]; double hum = myObject["hum"]; double pres = myObject["pres"]; Serial.print("Node: "); Serial.println(node); Serial.print("Temperature: "); Serial.print(temp); Serial.println(" C"); Serial.print("Humidity: "); Serial.print(hum); Serial.println(" %"); Serial.print("Pressure: "); Serial.print(pres); Serial.println(" hpa"); } void newConnectionCallback(uint32_t nodeId) { Serial.printf("New Connection, nodeId = %u\n", nodeId); } void changedConnectionCallback() { Serial.printf("Changed connections\n"); } void nodeTimeAdjustedCallback(int32_t offset) { Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset); } void setup() { Serial.begin(115200); initBME(); //mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT ); mesh.onReceive(&receivedCallback); mesh.onNewConnection(&newConnectionCallback); mesh.onChangedConnections(&changedConnectionCallback); mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback); userScheduler.addTask(taskSendMessage); taskSendMessage.enable(); } void loop() { // it will run the user scheduler as well mesh.update(); }
The code is compatible with both the ESP32 and ESP8266 boards.
Continue reading this section to learn how the code works.
Start by including the required libraries: the Adafruit_Sensor and Adafruit_BME280 to interface with the BME280 sensor; the painlessMesh library to handle the mesh network and the Arduino_JSON to create and handle JSON strings easily.
#include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> #include "painlessMesh.h" #include <Arduino_JSON.h>
Insert the mesh details in the following lines.
#define MESH_PREFIX "RNTMESH" //name for your MESH #define MESH_PASSWORD "MESHpassword" //password for your MESH #define MESH_PORT 5555 //default port
The MESH_PREFIX refers to the name of the mesh. You can change it to whatever you like. The MESH_PASSWORD, as the name suggests is the mesh password. You can change it to whatever you like. All nodes in the mesh should use the same MESH_PREFIX and MESH_PASSWORD.
The MESH_PORT refers to the the TCP port that you want the mesh server to run on. The default is 5555.
Create an Adafruit_BME280 object called bme on the default ESP32 or ESP8266 pins.
Adafruit_BME280 bme;In the nodeNumber variable insert the node number for your board. It must be a different number for each board.
int nodeNumber = 2;
The readings variable will be used to save the readings to be sent to the other boards.
String readings;The following line creates a new Scheduler called userScheduler.
Scheduler userScheduler; // to control your personal task
Create a painlessMesh object called mesh to handle the mesh network.
Create a task called taskSendMessage responsible for calling the sendMessage() function every five seconds as long as the program is running.
Task taskSendMessage(TASK_SECOND * 5 , TASK_FOREVER, &sendMessage);
The getReadings() function gets temperature, humidity and pressure readings from the BME280 sensor and concatenates all the information, including the node number on a JSON variable called jsonReadings.
JSONVar jsonReadings; jsonReadings["node"] = nodeNumber; jsonReadings["temp"] = bme.readTemperature(); jsonReadings["hum"] = bme.readHumidity(); jsonReadings["pres"] = bme.readPressure()/100.0F;
The following line shows the structure of the jsonReadings variable with arbitrary values.
{ "node":2, "temperature":24.51, "humidity":52.01, "pressure":1005.21 }
The jsonReadings variable is then converted into a JSON string using the stringify() method and saved on the readings variable.
readings = JSON.stringify(jsonReadings);
This variable is then returned by the function.
return readings;
The sendMessage() function sends the JSON string with the readings and node number (getReadings()) to all nodes in the network (broadcast).
void sendMessage () { String msg = getReadings(); mesh.sendBroadcast(msg); }
The initBME() function initializes the BME280 sensor.
void initBME(){ if (!bme.begin(0x76)) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } }
Next, several callback functions are created that will be called when some event on the mesh happens.
The receivedCallback() function prints the message sender (from) and the content of the message (msg.c_str()).
void receivedCallback( uint32_t from, String &msg ) { Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str());
The message comes in JSON format, so, we can access the variables as follows:
JSONVar myObject = JSON.parse(msg.c_str()); int node = myObject["node"]; double temp = myObject["temp"]; double hum = myObject["hum"]; double pres = myObject["pres"];
Finally, print all the information on the Serial Monitor.
Serial.print("Node: "); Serial.println(node); Serial.print("Temperature: "); Serial.print(temp); Serial.println(" C"); Serial.print("Humidity: "); Serial.print(hum); Serial.println(" %"); Serial.print("Pressure: "); Serial.print(pres); Serial.println(" hpa");
The newConnectionCallback() function runs whenever a new node joins the network. This function simply prints the chip ID of the new node. You can modify the function to do any other task.
void newConnectionCallback(uint32_t nodeId) { Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId); }
The changedConnectionCallback() function runs whenever a connection changes on the network (when a node joins or leaves the network).
void changedConnectionCallback() { Serial.printf("Changed connections\n"); }
The nodeTimeAdjustedCallback() function runs when the network adjusts the time, so that all nodes are synchronized. It prints the offset.
void nodeTimeAdjustedCallback(int32_t offset) { Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset); }
In the setup(), initialize the serial monitor.
void setup() { Serial.begin(115200);
Call the initBME() function to initialize the BME280 sensor.
initBME();
Choose the desired debug message types:
//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on mesh.setDebugMsgTypes( ERROR | STARTUP ); // set before init() so that you can see startup messages
Initialize the mesh with the details defined earlier.
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
Assign all the callback functions to their corresponding events.
mesh.onReceive(&receivedCallback); mesh.onNewConnection(&newConnectionCallback); mesh.onChangedConnections(&changedConnectionCallback); mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
Finally, add the taskSendMessage function to the userScheduler. The scheduler is responsible for handling and running the tasks at the right time.
userScheduler.addTask(taskSendMessage);
Finally, enable the taskSendMessage, so that the program starts sending the messages to the mesh.
taskSendMessage.enable();
To keep the mesh running, add mesh.update() to the loop().
void loop() { // it will run the user scheduler as well mesh.update(); }
After uploading the code to all your boards (each board with a different node number), you should see that each board is receiving the other boards’ messages.
The following screenshot shows the messages received by node 1. It receives the sensor readings from node 2, 3 and 4.

Copyright ©2025. All Rights Reserved Emblab THE RAVE INNOVATION