Skip to content

USER_GUIDE

github-actions[bot] edited this page Sep 7, 2026 · 2 revisions

AlteriomPainlessMesh 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.

Table of Contents

  1. Introduction
  2. Getting Started
  3. Core Concepts
  4. API Reference
  5. Alteriom Extensions
  6. Bridge to Internet
  7. Advanced Features
  8. Troubleshooting
  9. Examples

Introduction

What is AlteriomPainlessMesh?

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.

Key Features

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

Why Use painlessMesh?

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

Getting Started

What You'll Need

  • Hardware: 2 or more ESP8266 or ESP32 development boards
  • Software: Arduino IDE or PlatformIO
  • Accessories: USB cables for programming

Installation

Arduino Library Manager

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):

  1. Go to Releases
  2. Download the latest release ZIP file
  3. In Arduino IDE: Sketch → Include Library → Add .ZIP Library...
  4. Select the downloaded ZIP file
  5. Restart Arduino IDE

Or via Git Clone:

cd ~/Arduino/libraries/
git clone https://github.com/Alteriom/painlessMesh.git AlteriomPainlessMesh
# Restart Arduino IDE

PlatformIO

Add to your platformio.ini (the library is sparck75/AlteriomPainlessMesh on the PlatformIO registry):

lib_deps =
    sparck75/AlteriomPainlessMesh@^2.0.0

To follow the integration branch instead of a release, use the repository URL: https://github.com/Alteriom/painlessMesh.git#Feat/next-release.

Dependencies

painlessMesh requires these libraries (auto-installed with PlatformIO):

Arduino cores: ESP32 core 2.0.x or 3.x (the ESP32-C5 and ESP32-C6 need 3.x); ESP8266 core 3.x.

Your First Mesh Network

Step 1: Basic Example Code

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();
}

Step 2: Upload and Test

  1. Upload the code to your first ESP device
  2. Open Serial Monitor (115200 baud)
  3. Upload the same code to your second device
  4. Watch them automatically discover each other!

Expected Output

New Connection, nodeId = 123456789
Received from 123456789: Hello from node 123456789
Changed connections
Adjusted time 1234567. Offset = 12

Core Concepts

Mesh Networking Basics

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:

  1. Node Discovery: Nodes scan for other nodes broadcasting the same MESH_PREFIX
  2. Automatic Connection: Nodes connect to the strongest available signals
  3. Route Building: Each node maintains a routing table of the network topology
  4. Message Routing: Messages are automatically routed through intermediate nodes
  5. Self-Healing: When nodes disconnect, routes automatically reconfigure

Node Identification

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);

Message Types

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!");

Time Synchronization

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

Network Topology

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.


API Reference

painlessMesh Class

Initialization

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_STA for a leaf that accepts no children, or WIFI_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
}

Message Sending

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 argument

Since 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.

Callbacks

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);
}

Network Information

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();

Network Measurement

startDelayMeas() - Measure network delay

bool startDelayMeas(uint32_t nodeId);

Sends a packet to measure round-trip delay. Result delivered via onNodeDelayReceived() callback.

Debug Configuration

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);

TCP Retry Configuration

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

Overview

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

SensorPackage (Type 200)

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

CommandPackage (Type 400)

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

StatusPackage (Type 202)

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

Advanced Packages

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.


Bridge to Internet

Overview

Connect your mesh network to the Internet by creating a bridge node that connects to both the mesh and your WiFi router.

Quick Start (Auto Channel Detection)

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.)
}

Bridge Failover

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
  }
}

Shared Gateway Mode

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.


Advanced Features

Message Queue

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 counts

Priority Levels (PRIORITY_CRITICAL, PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_LOW):

  • PRIORITY_CRITICAL - Never evicted
  • PRIORITY_LOW - Evicted first when the queue is full, then NORMAL, then HIGH

See examples/priority/priority_with_queue.

Broadcast OTA

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.

MQTT Bridge

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.

Multi-Bridge Coordination

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

Troubleshooting

Common Issues

Nodes Not Connecting

Symptoms: Nodes don't discover each other

Solutions:

  1. Verify MESH_PREFIX and MESH_PASSWORD are identical
  2. Check devices are within WiFi range (~50-100m)
  3. Ensure MESH_PORT is the same on all devices
  4. Try different WiFi channels (avoid congested channels)
  5. Check for physical obstacles blocking signal

Messages Not Received

Symptoms: Messages sent but not received

Solutions:

  1. Check message size (keep under 1KB for reliability)
  2. Verify callback is registered: mesh.onReceive(&callback)
  3. Add debug output: mesh.setDebugMsgTypes(ERROR | COMMUNICATION)
  4. Ensure mesh.update() is called in loop()
  5. Check for network congestion (reduce message frequency)

High Memory Usage

Symptoms: Device crashes or becomes unstable

Solutions:

  1. Monitor free heap: ESP.getFreeHeap()
  2. Reduce message frequency
  3. Limit mesh size (max ~50 nodes for ESP8266)
  4. Use shorter messages
  5. Clear unused String objects
  6. Reduce TaskScheduler task count

Bridge Connection Issues

Symptoms: Bridge can't maintain router connection

Solutions:

  1. Use initAsBridge() for automatic channel detection
  2. Ensure router channel matches mesh channel
  3. Check router signal strength (RSSI > -70 dBm)
  4. Enable bridge debug: mesh.setDebugMsgTypes(ERROR | CONNECTION)
  5. Try static IP configuration on router

Debug Configuration

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);
}

Performance Optimization

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}";

Best Practices

  1. Avoid delay() - Use TaskScheduler instead
  2. Conservative messaging - Don't spam the network
  3. Handle failures gracefully - Messages can be dropped
  4. Monitor memory - Especially on ESP8266
  5. Use retries for critical messages - Implement acknowledgments
  6. Test at scale - Test with expected number of nodes

For more troubleshooting help, see:


Examples

Basic Mesh Network

#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();
}

Sensor Network with Alteriom Packages

#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();
}

Bridge with MQTT

#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();
}

More Examples

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:

IoT & Sensors:

Internet Connectivity:

Reliable Delivery (2.0):

Advanced Features:


Additional Resources

Documentation

Community & Support

Development


Version Information

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

Clone this wiki locally