Repository navigation
USER_GUIDE
Complete guide to building mesh networks with ESP8266/ESP32 devices
This comprehensive guide covers everything you need to build production-ready mesh networks using the AlteriomPainlessMesh library.
- Introduction
- Getting Started
- Core Concepts
- API Reference
- Alteriom Extensions
- Bridge to Internet
- Advanced Features
- Troubleshooting
- Examples
AlteriomPainlessMesh is a user-friendly library for creating self-organizing mesh networks with ESP8266 and ESP32 devices. This Alteriom fork extends the original painlessMesh library with specialized packages for IoT sensor networks, device control, and status monitoring.
Core Features:
- Automatic Mesh Formation - Nodes discover and connect automatically
- Self-Healing Network - Adapts when nodes join/leave
- Time Synchronization - Coordinated actions across all nodes
- Smart Routing - Broadcast, point-to-point, and neighbor messaging
- Plugin System - Type-safe custom message packages
- ESP32 & ESP8266 Support - Full support for both platforms
- Memory Efficient - Optimized for resource-constrained devices
Alteriom Extensions:
- Broadcast OTA Distribution - 98% network traffic reduction for large meshes
- MQTT Bridge - Professional monitoring with Grafana/InfluxDB/Prometheus
- Structured Packages - SensorPackage, CommandPackage, StatusPackage
- Bridge Failover - Automatic high-availability for critical systems
- Message Queue - Zero data loss during Internet outages
True Ad-Hoc Networking
- No planning, central controller, or router required
- Any system of 1 or more nodes will self-organize
- Maximum mesh size limited only by available memory
JSON-Based Messaging
- Human-readable and easy to understand
- Simple integration with web applications
- Straightforward debugging and development
Production Ready
- Tested in industrial deployments
- Enterprise-grade stability and performance
- Comprehensive error handling and recovery
- Hardware: 2 or more ESP8266 or ESP32 development boards
- Software: Arduino IDE or PlatformIO
- Accessories: USB cables for programming
Search for "Alteriom PainlessMesh" in Tools → Manage Libraries... and install it with the dependencies it offers; also install AsyncTCP (ESP32) or ESPAsyncTCP (ESP8266). The index follows a GitHub release within a day.
Manual installation (a release ZIP, or a clone to follow a branch):
- Go to Releases
- Download the latest release ZIP file
- In Arduino IDE: Sketch → Include Library → Add .ZIP Library...
- Select the downloaded ZIP file
- Restart Arduino IDE
Or via Git Clone:
cd ~/Arduino/libraries/
git clone https://github.com/Alteriom/painlessMesh.git AlteriomPainlessMesh
# Restart Arduino IDEAdd to your platformio.ini (the library is sparck75/AlteriomPainlessMesh on the PlatformIO registry):
lib_deps =
sparck75/AlteriomPainlessMesh@^2.0.0To follow the integration branch instead of a release, use the repository URL: https://github.com/Alteriom/painlessMesh.git#Feat/next-release.
painlessMesh requires these libraries (auto-installed with PlatformIO):
- ArduinoJson v7.x
- TaskScheduler v4.x
- ESPAsyncTCP (ESP8266) v2.x
- AsyncTCP (ESP32) v3.4.7 or later
-
PubSubClient is only needed by the
mqttBridgeexample
Arduino cores: ESP32 core 2.0.x or 3.x (the ESP32-C5 and ESP32-C6 need 3.x); ESP8266 core 3.x.
Copy this code to your Arduino IDE:
#include "painlessMesh.h"
#define MESH_PREFIX "MyMeshNetwork"
#define MESH_PASSWORD "somethingSneaky"
#define MESH_PORT 5555
Scheduler userScheduler;
painlessMesh mesh;
// Task to send messages
Task taskSendMessage(TASK_SECOND * 1, TASK_FOREVER, &sendMessage);
void sendMessage() {
String msg = "Hello from node ";
msg += mesh.getNodeId();
mesh.sendBroadcast(msg);
taskSendMessage.setInterval(random(TASK_SECOND * 1, TASK_SECOND * 5));
}
void receivedCallback(uint32_t from, String &msg) {
Serial.printf("Received from %u: %s\n", from, msg.c_str());
}
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);
mesh.setDebugMsgTypes(ERROR | STARTUP);
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() {
mesh.update();
}- Upload the code to your first ESP device
- Open Serial Monitor (115200 baud)
- Upload the same code to your second device
- Watch them automatically discover each other!
New Connection, nodeId = 123456789
Received from 123456789: Hello from node 123456789
Changed connections
Adjusted time 1234567. Offset = 12
What is a Mesh Network?
A mesh network is a network topology where each node can relay data for other nodes. Unlike traditional star or tree topologies with a central hub, mesh networks are decentralized and self-organizing.
How painlessMesh Works:
- Node Discovery: Nodes scan for other nodes broadcasting the same MESH_PREFIX
- Automatic Connection: Nodes connect to the strongest available signals
- Route Building: Each node maintains a routing table of the network topology
- Message Routing: Messages are automatically routed through intermediate nodes
- Self-Healing: When nodes disconnect, routes automatically reconfigure
Node ID:
- Each ESP device has a unique 32-bit chip ID
- Retrieved automatically using
system_get_chip_id() - Used for targeted messaging:
mesh.sendSingle(nodeId, msg)
Getting Your Node ID:
uint32_t myNodeId = mesh.getNodeId();
Serial.printf("My Node ID: %u\n", myNodeId);Broadcast Messages:
- Sent to all nodes in the mesh
- Propagate through the entire network
- Use for sensor data, status updates, alerts
mesh.sendBroadcast("Hello everyone!");Single (Targeted) Messages:
- Sent to a specific node by ID
- Automatically routed through the mesh
- Use for commands, replies, private data
uint32_t targetNode = 123456789;
mesh.sendSingle(targetNode, "Hello specific node!");Mesh Time:
- All nodes maintain a synchronized timebase
- Uses SNTP-based protocol for synchronization
- Returns microseconds since mesh startup
- Rolls over every 71 minutes
uint32_t meshTime = mesh.getNodeTime();
Serial.printf("Mesh time: %u µs\n", meshTime);Use Cases:
- Coordinated LED displays
- Synchronized sensor readings
- Event scheduling across nodes
- Timestamp correlation
Getting Node List:
std::list<uint32_t> nodes = mesh.getNodeList();
Serial.printf("Connected nodes: %d\n", nodes.size());
for (auto nodeId : nodes) {
Serial.printf(" - Node: %u\n", nodeId);
}Topology as JSON:
String topology = mesh.subConnectionJson();
Serial.println(topology);Checking Connections:
uint32_t nodeId = 123456789;
if (mesh.isConnected(nodeId)) {
Serial.println("Node is connected");
}Note: Connection status can change rapidly in dynamic mesh networks. This method reflects the current state at the moment of the call.
init() - Initialize the mesh network
void init(String ssid, String password, uint16_t port = 5555,
WiFiMode_t connectMode = WIFI_AP_STA, uint8_t channel = 1,
uint8_t hidden = 0, uint8_t maxconn = MAX_CONN,
String stationSSID = "", String stationPassword = "");
void init(String ssid, String password, Scheduler* scheduler, uint16_t port = 5555,
WiFiMode_t connectMode = WIFI_AP_STA, uint8_t channel = 1,
uint8_t hidden = 0, uint8_t maxconn = MAX_CONN,
String stationSSID = "", String stationPassword = "");Parameters:
-
ssid- Network name (same for all nodes) -
password- Network password -
scheduler- Your TaskScheduler instance; without one the library creates its own -
port- TCP port for mesh communication (default: 5555) -
connectMode-WIFI_AP_STA(default),WIFI_STAfor a leaf that accepts no children, orWIFI_AP -
channel- WiFi channel (1-13), or 0 to scan for the mesh and join it on whatever channel it is on -
hidden- Hide the mesh SSID -
maxconn- Children this node's AP accepts (0 makes it a leaf; see the ESP8266 note in the README) -
stationSSID,stationPassword- Also connect the station to a router (a bridge);initAsBridge()is the usual way
Example:
Scheduler userScheduler;
mesh.init("MyMesh", "password123", &userScheduler, 5555);stop() - Stop the node
void stop();Disconnects from all nodes and stops sending/receiving messages.
update() - Main mesh update loop
void update();Call this in your loop() function. Handles all mesh maintenance tasks.
void loop() {
mesh.update(); // Required
}sendBroadcast() - Send to all nodes
bool sendBroadcast(String &msg, bool includeSelf = false);Parameters:
-
msg- Message to broadcast -
includeSelf- Send to yourself (default: false)
Returns: true if successful
sendSingle() - Send to specific node
bool sendSingle(uint32_t dest, String &msg);Parameters:
-
dest- Destination node ID -
msg- Message to send
Returns: true if successful
Delivery confirmation (v2.0.0+) - Both send functions accept an optional delivery callback for reliable communication:
bool sendSingle(uint32_t dest, String &msg,
painlessmesh::ack::deliveryCallback_t ackCallback,
uint32_t ackTimeoutMs = 5000);
bool sendBroadcast(String &msg, bool includeSelf,
painlessmesh::ack::deliveryCallback_t ackCallback,
uint32_t ackTimeoutMs = 5000);When a callback is given, the receiving node automatically acknowledges the
message and the callback fires with delivered = true and the round-trip
latency, or delivered = false after the timeout. For broadcasts the
callback fires once per node in the mesh. See the reliableSensorLogging
and commandControl examples:
mesh.sendSingle(gatewayId, msg,
[](uint32_t nodeId, bool delivered, uint32_t latencyMs) {
if (delivered)
Serial.printf("Node %u confirmed in %u ms\n", nodeId, latencyMs);
else
Serial.printf("Delivery to %u timed out\n", nodeId);
});Use mesh.pendingAcks() to see how many messages are still unconfirmed and
mesh.checkAcks() to poll timeouts manually (they are also processed inside
mesh.update()). At most PAINLESSMESH_MAX_PENDING_ACKS (default 32)
messages can await confirmation at once — beyond that the send returns
false and nothing is sent.
To combine a priority level with a delivery callback in one call, pass a
painlessmesh::SendOptions struct instead of using the separate overloads:
painlessmesh::SendOptions options;
options.priority = painlessmesh::protocol::PRIORITY_HIGH;
options.ackCallback = [](uint32_t nodeId, bool delivered, uint32_t latencyMs) {
// ...
};
mesh.sendSingle(targetNode, msg, options);
mesh.sendBroadcast(msg, options); // includeSelf as optional third argumentSince v2.0.0 the priority level is carried on the wire, so intermediate nodes forward a prioritized message at the sender's priority instead of dropping it to normal after the first hop.
onReceive() - Message received callback
void onReceive(receivedCallback_t callback);Callback signature:
void receivedCallback(uint32_t from, String &msg) {
Serial.printf("From %u: %s\n", from, msg.c_str());
}onNewConnection() - New node connected
void onNewConnection(newConnectionCallback_t callback);Callback signature:
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("New node: %u\n", nodeId);
}onChangedConnections() - Topology changed
void onChangedConnections(changedConnectionsCallback_t callback);Callback signature:
void changedConnectionCallback() {
Serial.println("Network topology changed");
}onNodeTimeAdjusted() - Time synchronized
void onNodeTimeAdjusted(nodeTimeAdjustedCallback_t callback);Callback signature:
void nodeTimeAdjustedCallback(int32_t offset) {
Serial.printf("Time adjusted: %d µs\n", offset);
}onNodeDelayReceived() - Delay measurement response
void onNodeDelayReceived(nodeDelayCallback_t callback);Callback signature:
void onDelayReceived(uint32_t nodeId, int32_t delay) {
Serial.printf("Delay to %u: %d µs\n", nodeId, delay);
}getNodeId() - Get this node's ID
uint32_t getNodeId();getNodeList() - Get list of all known nodes
std::list<uint32_t> getNodeList();getNodeTime() - Get mesh timebase (microseconds)
uint32_t getNodeTime();isConnected() - Check if node is connected
bool isConnected(uint32_t nodeId);subConnectionJson() - Get topology as JSON
String subConnectionJson();startDelayMeas() - Measure network delay
bool startDelayMeas(uint32_t nodeId);Sends a packet to measure round-trip delay. Result delivered via onNodeDelayReceived() callback.
setDebugMsgTypes() - Configure debug output
void setDebugMsgTypes(uint16_t types);Debug types:
-
ERROR- Error messages -
STARTUP- Startup information -
CONNECTION- Connection events -
SYNC- Time synchronization -
COMMUNICATION- Message routing -
GENERAL- General information -
MSG_TYPES- Message type details -
REMOTE- Remote node information
Example:
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);setTcpRetryConfig() / getTcpRetryConfig() - Tune how a node retries
a failed TCP connection before falling back to a full WiFi reconnect.
void setTcpRetryConfig(const painlessmesh::tcp::TcpRetryConfig& config);
painlessmesh::tcp::TcpRetryConfig getTcpRetryConfig() const;| Field | Default | Meaning |
|---|---|---|
maxRetries |
5 | TCP connect attempts after the first before giving up |
retryDelayMs |
1000 | Base delay between retries; scaled 1x, 2x, 4x, 8x, 8x |
stabilizationDelayMs |
500 | Wait after IP acquisition before the first attempt |
exhaustionReconnectDelayMs |
10000 | Wait before the WiFi reconnect after exhaustion |
failureBlockDurationMs |
60000 | How long a failed peer is skipped during AP selection |
The defaults reproduce the previous hardcoded behaviour exactly, so existing
sketches see no change. Call this before mesh.init().
Example (real-time profile — fail fast and re-scan rather than stall):
painlessmesh::tcp::TcpRetryConfig cfg; // starts at the defaults
cfg.maxRetries = 1;
cfg.retryDelayMs = 200;
cfg.exhaustionReconnectDelayMs = 1000;
mesh.setTcpRetryConfig(cfg);
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);maxRetries is capped at 10 and retryDelayMs is held between 50 ms and
60000 ms; use getTcpRetryConfig() to read back what actually took effect.
See examples/tcpRetryConfig/ for real-time, high-reliability and
battery-saver profiles, and the API docs for the full tuning guidance.
Note: these production defaults prevent connection churn and rapid reconnect loops. Tuning them down can reintroduce mesh instability.
Alteriom extensions provide production-ready, type-safe packages for common IoT scenarios:
- Environmental Monitoring - Temperature, humidity, pressure sensors
- Device Control - Commands for actuators, displays, relays
- Health Monitoring - Device status, diagnostics, telemetry
- Type Safety - Compile-time validation
- Production Ready - Tested and optimized implementations
For broadcasting environmental sensor data.
Fields:
-
temperature(double) - Temperature in °C -
humidity(double) - Relative humidity (%) -
pressure(double) - Atmospheric pressure (hPa) -
sensorId(uint32_t) - Sensor identifier -
timestamp(uint32_t) - Measurement time (mesh time) -
batteryLevel(uint8_t) - Battery percentage (0-100)
Example:
#include "examples/alteriom/alteriom_sensor_package.hpp"
using namespace alteriom;
SensorPackage sensor;
sensor.from = mesh.getNodeId();
sensor.temperature = 23.5;
sensor.humidity = 65.0;
sensor.pressure = 1013.25;
sensor.sensorId = 1001;
sensor.timestamp = mesh.getNodeTime();
sensor.batteryLevel = 85;
String json = sensor.toJsonString();
mesh.sendBroadcast(json);Receiving:
void receivedCallback(uint32_t from, String &msg) {
JsonDocument doc;
deserializeJson(doc, msg);
if (doc["type"] == 200) {
SensorPackage sensor(doc.as<JsonObject>());
Serial.printf("Sensor %u: %.1f°C, %.1f%% RH, %.1f hPa\n",
sensor.sensorId, sensor.temperature,
sensor.humidity, sensor.pressure);
}
}Use Cases:
- Weather stations
- Environmental monitoring
- HVAC feedback
- Greenhouse automation
- Industrial sensors
For sending control commands to specific devices.
Fields:
-
command(uint8_t) - Command type -
targetDevice(uint32_t) - Device identifier -
parameters(String) - JSON parameters -
commandId(uint32_t) - Unique command ID
Example:
CommandPackage cmd;
cmd.dest = targetNodeId;
cmd.from = mesh.getNodeId();
cmd.command = 1; // LED_CONTROL
cmd.targetDevice = 100;
cmd.parameters = "{\"brightness\":75,\"color\":\"blue\"}";
cmd.commandId = generateCommandId();
String json = cmd.toJsonString();
mesh.sendSingle(targetNodeId, json);Handling Commands:
void receivedCallback(uint32_t from, String &msg) {
JsonDocument doc;
deserializeJson(doc, msg);
if (doc["type"] == 400) {
CommandPackage cmd(doc.as<JsonObject>());
if (cmd.dest != mesh.getNodeId()) return;
// Parse parameters
JsonDocument params;
deserializeJson(params, cmd.parameters);
int brightness = params["brightness"];
String color = params["color"];
// Execute command
setLED(cmd.targetDevice, brightness, color);
}
}Use Cases:
- Remote device control
- Actuator management
- Display updates
- System configuration
- Automation triggers
For broadcasting device health and operational status.
Fields:
-
deviceStatus(uint8_t) - Status flags -
uptime(uint32_t) - Uptime in seconds -
freeMemory(uint16_t) - Free heap memory (bytes) -
wifiStrength(uint8_t) - WiFi RSSI (dBm) -
firmwareVersion(String) - Firmware version
Example:
StatusPackage status;
status.from = mesh.getNodeId();
status.deviceStatus = 1; // OPERATIONAL
status.uptime = millis() / 1000;
status.freeMemory = ESP.getFreeHeap();
status.wifiStrength = WiFi.RSSI();
status.firmwareVersion = "2.0.0";
String json = status.toJsonString();
mesh.sendBroadcast(json);Use Cases:
- System monitoring
- Predictive maintenance
- Network diagnostics
- Performance tracking
- Remote troubleshooting
MetricsPackage (Type 204) - Comprehensive performance metrics
- CPU usage, memory health, network throughput
- Response time and latency tracking
- Connection quality monitoring
HealthCheckPackage (Type 605) - Proactive problem detection
- Overall health scoring (0-100)
- Problem flag indicators
- Memory leak detection
- Predictive maintenance
Bridge Packages:
- BridgeStatusPackage (Type 610) - Bridge health monitoring
- BridgeElectionPackage (Type 611) - Automatic failover election
- BridgeTakeoverPackage (Type 612) - Bridge role announcement
- NTPTimeSyncPackage (Type 614) - NTP time synchronization
See README.md for complete package documentation.
Connect your mesh network to the Internet by creating a bridge node that connects to both the mesh and your WiFi router.
The recommended approach uses initAsBridge() which automatically detects your router's channel:
#include "painlessMesh.h"
#define MESH_PREFIX "MyMeshNetwork"
#define MESH_PASSWORD "somethingSneaky"
#define ROUTER_SSID "YourRouterSSID"
#define ROUTER_PASSWORD "YourRouterPassword"
Scheduler userScheduler;
painlessMesh mesh;
void setup() {
Serial.begin(115200);
// Automatically detects router channel and configures mesh
mesh.initAsBridge(MESH_PREFIX, MESH_PASSWORD,
ROUTER_SSID, ROUTER_PASSWORD,
&userScheduler, 5555);
mesh.onReceive(&receivedCallback);
}
void loop() {
mesh.update();
}
void receivedCallback(uint32_t from, String &msg) {
Serial.printf("Mesh message from %u: %s\n", from, msg.c_str());
// Forward to Internet services (HTTP, MQTT, etc.)
}Automatic high-availability for critical systems:
Features:
- RSSI-based election (best router signal wins; ties go to uptime, free memory, node ID)
- A bridge that stops cleanly (
mesh.stop(), or a sketch that stops the mesh before rebooting) announces it is leaving, and the candidates elect within seconds - A bridge that loses power is noticed when its last status ages out: the bridge timeout (60 s) plus up to one 30 s monitor tick
- The elected bridge announces its router channel; peers move to it and treat it as home
- One election at a time, a 60 s hold between role changes, and a minimum router RSSI for a lone candidate
The first election check comes after a startup period (setElectionStartupDelay(), 60 s by default). See examples/bridge_failover for the whole protocol.
Example:
// Enable automatic bridge failover
mesh.setRouterCredentials(ROUTER_SSID, ROUTER_PASSWORD);
mesh.enableBridgeFailover(true);
mesh.onBridgeRoleChanged(&bridgeRoleCallback);
void bridgeRoleCallback(bool isBridge, const String& reason) {
if (isBridge) {
Serial.printf("🎯 Promoted to bridge: %s\n", reason.c_str());
// Start MQTT connection, etc.
} else {
Serial.printf("Demoted from bridge: %s\n", reason.c_str());
// Stop bridge services
}
}All nodes as Internet gateways with automatic failover:
Features:
- Universal Internet access (all nodes connect to router)
- Instant failover through mesh relay
- No single point of failure
- Automatic gateway election
- Duplicate prevention
Example:
// Initialize all nodes as shared gateways
mesh.initAsSharedGateway(
MESH_PREFIX, MESH_PASSWORD,
ROUTER_SSID, ROUTER_PASSWORD,
&userScheduler, MESH_PORT
);
// Send data with automatic failover
mesh.sendToInternet(
"https://api.example.com/data",
sensorJson,
[](bool success, uint16_t status, String error) {
Serial.printf("Delivery: %s\n", success ? "OK" : error.c_str());
}
);For complete bridge documentation, see BRIDGE_TO_INTERNET.md.
Zero data loss during Internet outages:
// Enable the queue with room for 100 messages (call after mesh.init())
mesh.enableMessageQueue(true, 100);
// Queue a message for an Internet endpoint. Returns the queued message's
// id, 0 on failure. The queue holds it until you drain it.
String criticalAlarm = "{\"sensor\":\"O2\",\"value\":2.5,\"alarm\":true}";
uint32_t id = mesh.queueMessage(criticalAlarm, "https://api.example.com/alarm", PRIORITY_CRITICAL);
// Drain it when a gateway with Internet is available: flushMessageQueue()
// returns the queued messages, your sketch sends each one and removes it
if (mesh.hasInternetConnection()) {
for (auto& queued : mesh.flushMessageQueue()) {
mesh.sendToInternet(queued.destination, queued.payload, onResult);
mesh.removeQueuedMessage(queued.id);
}
}
auto stats = mesh.getQueueStats(); // queued, dropped and flushed countsPriority Levels (PRIORITY_CRITICAL, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_LOW):
-
PRIORITY_CRITICAL- Never evicted -
PRIORITY_LOW- Evicted first when the queue is full, thenNORMAL, thenHIGH
See examples/priority/priority_with_queue.
Efficient firmware distribution for large meshes:
// Receiver: accept firmware offered for this role, report progress
mesh.initOTAReceive("sensor", [](int part, int total) {
Serial.printf("OTA %d/%d\n", part, total);
});
// Sender: serve the image in parts on request, then offer it to the role
mesh.initOTASend(
[](painlessmesh::plugin::ota::DataRequest pkg, char* buffer) {
// copy part pkg.partNo of the image into buffer, return its length
return (size_t)readPart(pkg.partNo, buffer);
},
OTA_PART_SIZE);
mesh.offerOTA("sensor", "esp32", md5, partCount, false, true /* broadcast */);With the last argument true the image is broadcast once instead of unicast to each node, which is what makes a large mesh affordable. See examples/otaSender and examples/otaReceiver for a complete pair.
Professional monitoring integration:
The library has no MQTT client of its own; the bridge sketch uses PubSubClient beside the mesh:
WiFiClient espClient;
PubSubClient mqttClient(espClient);
// Publish mesh data
mesh.onReceive([](uint32_t from, String &msg) {
String topic = "mesh/" + String(from) + "/data";
mqttClient.publish(topic.c_str(), msg.c_str());
});
// Forward commands from mesh/<nodeId>/command into the mesh
mqttClient.setCallback([](char* topic, byte* payload, unsigned int length) {
String t(topic);
uint32_t nodeId = t.substring(t.indexOf('/') + 1, t.lastIndexOf('/')).toInt();
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
mesh.sendSingle(nodeId, msg);
});
mqttClient.subscribe("mesh/+/command");See the complete sketch in the Bridge with MQTT example below and in examples/mqttBridge.
Enterprise load balancing:
// Primary bridge (priority 10)
mesh.initAsBridge(MESH_PREFIX, MESH_PASSWORD,
ROUTER_SSID, ROUTER_PASSWORD,
&userScheduler, MESH_PORT, 10);
// Configure load balancing
mesh.setBridgeSelectionStrategy(painlessMesh::ROUND_ROBIN);
// Monitor bridge coordination (fires every ~30s per bridge)
mesh.onBridgeCoordination(
[](const plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode) {
Serial.printf("Bridge %u: priority=%d, load=%d%%\n",
fromNode, pkg.priority, pkg.load);
}
);
// Get notified when bridge state changes
mesh.onBridgeCoordinationChanged(
[](const plugin::BridgeCoordinationPackage& pkg, uint32_t fromNode,
TSTRING changeType) {
Serial.printf("Bridge %s: %u (role=%s)\n",
changeType.c_str(), fromNode, pkg.role.c_str());
}
);Strategies:
-
PRIORITY_BASED- Use highest priority bridge -
ROUND_ROBIN- Distribute load evenly -
BEST_SIGNAL- Use best RSSI bridge
Symptoms: Nodes don't discover each other
Solutions:
- Verify MESH_PREFIX and MESH_PASSWORD are identical
- Check devices are within WiFi range (~50-100m)
- Ensure MESH_PORT is the same on all devices
- Try different WiFi channels (avoid congested channels)
- Check for physical obstacles blocking signal
Symptoms: Messages sent but not received
Solutions:
- Check message size (keep under 1KB for reliability)
- Verify callback is registered:
mesh.onReceive(&callback) - Add debug output:
mesh.setDebugMsgTypes(ERROR | COMMUNICATION) - Ensure
mesh.update()is called inloop() - Check for network congestion (reduce message frequency)
Symptoms: Device crashes or becomes unstable
Solutions:
- Monitor free heap:
ESP.getFreeHeap() - Reduce message frequency
- Limit mesh size (max ~50 nodes for ESP8266)
- Use shorter messages
- Clear unused String objects
- Reduce TaskScheduler task count
Symptoms: Bridge can't maintain router connection
Solutions:
- Use
initAsBridge()for automatic channel detection - Ensure router channel matches mesh channel
- Check router signal strength (RSSI > -70 dBm)
- Enable bridge debug:
mesh.setDebugMsgTypes(ERROR | CONNECTION) - Try static IP configuration on router
Enable verbose debugging:
void setup() {
Serial.begin(115200);
// Enable all debug messages
mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION |
SYNC | COMMUNICATION | GENERAL);
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
}Reduce Network Congestion:
// Use rate limiting
static unsigned long lastBroadcast = 0;
const unsigned long MIN_INTERVAL = 1000; // 1 second
if (millis() - lastBroadcast > MIN_INTERVAL) {
mesh.sendBroadcast(msg);
lastBroadcast = millis();
}Optimize Message Size:
// Bad: Large JSON with unnecessary data
String msg = "{\"temperature\":23.5,\"humidity\":65.0,\"description\":\"This is a very long description...\"}";
// Good: Compact JSON with essential data
String msg = "{\"t\":23.5,\"h\":65}";-
Avoid
delay()- Use TaskScheduler instead - Conservative messaging - Don't spam the network
- Handle failures gracefully - Messages can be dropped
- Monitor memory - Especially on ESP8266
- Use retries for critical messages - Implement acknowledgments
- Test at scale - Test with expected number of nodes
For more troubleshooting help, see:
#include "painlessMesh.h"
#define MESH_PREFIX "MyMesh"
#define MESH_PASSWORD "password"
#define MESH_PORT 5555
Scheduler userScheduler;
painlessMesh mesh;
Task taskSendMessage(TASK_SECOND * 5, TASK_FOREVER, [](){
String msg = "Hello from " + String(mesh.getNodeId());
mesh.sendBroadcast(msg);
});
void receivedCallback(uint32_t from, String &msg) {
Serial.printf("From %u: %s\n", from, msg.c_str());
}
void setup() {
Serial.begin(115200);
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
mesh.onReceive(&receivedCallback);
userScheduler.addTask(taskSendMessage);
taskSendMessage.enable();
}
void loop() {
mesh.update();
}#include "painlessMesh.h"
#include "examples/alteriom/alteriom_sensor_package.hpp"
using namespace alteriom;
#define MESH_PREFIX "SensorMesh"
#define MESH_PASSWORD "password"
#define MESH_PORT 5555
Scheduler userScheduler;
painlessMesh mesh;
// DHT22 sensor pins
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
Task taskSendSensor(30000, TASK_FOREVER, [](){
SensorPackage sensor;
sensor.from = mesh.getNodeId();
sensor.temperature = dht.readTemperature();
sensor.humidity = dht.readHumidity();
sensor.sensorId = mesh.getNodeId();
sensor.timestamp = mesh.getNodeTime();
sensor.batteryLevel = readBattery();
if (!isnan(sensor.temperature) && !isnan(sensor.humidity)) {
mesh.sendBroadcast(sensor.toJsonString());
}
});
void receivedCallback(uint32_t from, String &msg) {
JsonDocument doc;
deserializeJson(doc, msg);
if (doc["type"] == 200) {
SensorPackage sensor(doc.as<JsonObject>());
Serial.printf("Sensor %u: %.1f°C, %.1f%% RH\n",
sensor.sensorId, sensor.temperature, sensor.humidity);
}
}
void setup() {
Serial.begin(115200);
dht.begin();
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
mesh.onReceive(&receivedCallback);
userScheduler.addTask(taskSendSensor);
taskSendSensor.enable();
}
void loop() {
mesh.update();
}#include "painlessMesh.h"
#include <PubSubClient.h>
#define MESH_PREFIX "MyMesh"
#define MESH_PASSWORD "password"
#define ROUTER_SSID "YourRouter"
#define ROUTER_PASSWORD "RouterPassword"
Scheduler userScheduler;
painlessMesh mesh;
WiFiClient espClient;
PubSubClient mqttClient(espClient);
void receivedCallback(uint32_t from, String &msg) {
// Forward mesh messages to MQTT
String topic = "mesh/" + String(from) + "/data";
mqttClient.publish(topic.c_str(), msg.c_str());
}
void mqttCallback(char* topic, byte* payload, unsigned int length) {
// Forward MQTT messages to mesh
String msg = "";
for (unsigned int i = 0; i < length; i++) {
msg += (char)payload[i];
}
// Extract node ID from topic: mesh/<nodeId>/command
String topicStr = String(topic);
int startIdx = topicStr.indexOf("/") + 1;
int endIdx = topicStr.lastIndexOf("/");
String nodeIdStr = topicStr.substring(startIdx, endIdx);
uint32_t nodeId = nodeIdStr.toInt();
mesh.sendSingle(nodeId, msg);
}
void setup() {
Serial.begin(115200);
// Initialize bridge
mesh.initAsBridge(MESH_PREFIX, MESH_PASSWORD,
ROUTER_SSID, ROUTER_PASSWORD,
&userScheduler, 5555);
mesh.onReceive(&receivedCallback);
// Connect to MQTT
mqttClient.setServer("mqtt.example.com", 1883);
mqttClient.setCallback(mqttCallback);
mqttClient.connect("painlessMeshBridge");
mqttClient.subscribe("mesh/+/command");
}
void loop() {
mesh.update();
mqttClient.loop();
}Complete examples are available in the repository (18 example directories, 21 sketches, every one compiled for esp32 and esp8266 on each pull request):
Getting Started:
- Start Here - Best starting point for beginners
- Basic Example - Essential patterns and techniques
IoT & Sensors:
- Alteriom Sensor Node - IoT sensor packages
- Named Mesh - Using node names instead of IDs
Internet Connectivity:
- Bridge Example - Basic Internet bridge
- Bridge Failover - High availability bridge
- Shared Gateway - All nodes as gateways
- Send to Internet - Direct Internet communication
- MQTT Bridge - MQTT integration
Reliable Delivery (2.0):
- Reliable Sensor Logging - Buffered retries until the gateway confirms
- Command Control - Per-node broadcast confirmation
Advanced Features:
- Priority Queue - Message priority handling
- TCP Retry Config - Tuning connection retries
- OTA Sender - Firmware update distribution
- OTA Receiver - Firmware update reception
- Log Server - Centralized logging
- Log Client - Log forwarding
- Web Server - Embedded web interface
- Main README - Quick reference and overview
- BRIDGE_TO_INTERNET.md - Complete bridge guide
- API Documentation - Online docs
- CHANGELOG - Version history
- CONTRIBUTING - Contribution guidelines
- GitHub Issues - Bug reports and features
- GitHub Discussions - General discussion
- Release Guide - Release process
- Security Policy - Security information
- Code of Conduct - Community guidelines
This guide covers AlteriomPainlessMesh 2.0.0. Every snippet in it is written against the 2.0.0 headers; when the API changes, the guide changes in the same pull request.
For the latest updates and releases, visit:
Made with ❤️ by the painlessMesh community