Hello, I'm Ganesh. I'm building git-lrc, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. Star Us to help devs discover the project. Do give it a try and share your feedback for improving the product.
In this article, I will be demonstrating how to use PainlessMesh and MQTT with a NodeMCUv2 (ESP8266) board.
What Problem we are solving?
We will be reciving data from multiple nodes and send it to cloud using MQTT.
- Publish the data from received at gateway to cloud using MQTT
- Scallable MQTT topics publishing

How it works?
- Receive data from multiple nodes
These data will be in compressed in these format.
void onMeshReceive(const uint32_t &from, const String &msg) {
Serial.printf("[mesh] From %u: %s\n", from, msg.c_str());
JsonDocument doc;
if (deserializeJson(doc, msg) != DeserializationError::Ok) {
Serial.println("[mesh] JSON parse failed");
return;
}
const char *msgType = doc["type"] | "data";
String topic = buildTopic(from, msgType);
if (mqttClient.connected()) {
mqttClient.publish(topic.c_str(), msg.c_str());
Serial.printf("[mqtt] Published → %s\n", topic.c_str());
} else {
Serial.println("[mqtt] Not connected — message dropped");
}
}
- Publish the data from received at gateway to cloud using MQTT
Setup wifi credentials.
#define STATION_SSID "your_ssid"
#define STATION_PASSWORD "your_password"
Connect to wifi using painlessmesh.
As painlessmesh is heavy library it will consume lot of power and data.
So we will use mesh.stationManual() to connect to wifi.
mesh.stationManual(STATION_SSID,
STATION_PASSWORD); // ← key: mesh owns the STA
mesh.setHostname("ais-gateway")
- Scallable MQTT topics publishing
We will use MQTT to send data from gateway to cloud.
We will use these credentials for MQTT.
#define MQTT_HOST "your_mqtt_host"
#define MQTT_PORT_TLS 8883
#define MQTT_USER "your_mqtt_user"
#define MQTT_PASS "your_mqtt_password"
We will use these topics for MQTT.
Each topic will have different meaning.
For example we will use these topics for MQTT.
For sending an industrial sensor data we will use these topics.
#define T_VERSION "v1"
#define T_TENANT "electrotech"
#define T_INDUSTRY "pcba"
#define T_SITE "plant1"
#define T_GATEWAY "gw_01"
Finally followed with config message.
{
"type": "config",
"schema": {
"temp": {"type":"float","scale":10,"offset":0,"min":0,"max":51.1},
"hum": {"type":"float","scale":10,"offset":-20,"min":20,"max":122.3},
"gas": {"type":"float","scale":0.1,"offset":0,"min":0,"max":1023}
}
}
/**
* gateway.cpp — PainlessMesh Root + MQTT Bridge (HiveMQ TLS)
*
* Follows the official painlessMesh MQTT bridge pattern:
* - mesh.stationManual() hands WiFi STA management to the mesh library
* so it never disconnects from the router to scan for mesh peers.
* - MQTT connect is triggered by IP change detection in loop().
* - mesh.setRoot(true) called BEFORE init().
*
* Data flow: sensor node → mesh → gateway → HiveMQ MQTT → backend
* Topic format: v1/{tenant}/{industry}/{site}/{gateway}/{node}/{type}
*/
#include <Arduino.h>
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
#include <painlessMesh.h>
// ─── WiFi credentials ────────────────────────────────────────────────────────
#define STATION_SSID "your_ssid"
#define STATION_PASSWORD "your_password"
// ─── Mesh credentials ────────────────────────────────────────────────────────
#define MESH_PREFIX "your_mesh_prefix"
#define MESH_PASSWORD "your_mesh_password"
#define MESH_PORT 5555
// ─── MQTT credentials (from backend .env) ────────────────────────────────────
#define MQTT_HOST "your_mqtt_host"
#define MQTT_PORT_TLS 8883
#define MQTT_USER "your_mqtt_user"
#define MQTT_PASS "your_mqtt_password"
// ─── Topic routing constants ─────────────────────────────────────────────────
#define T_VERSION "v1"
#define T_TENANT "electrotech"
#define T_INDUSTRY "pcba"
#define T_SITE "plant1"
#define T_GATEWAY "gw_01"
// ─── Globals ─────────────────────────────────────────────────────────────────
painlessMesh mesh;
BearSSL::WiFiClientSecure wifiClient;
PubSubClient mqttClient(wifiClient);
IPAddress myIP(0, 0, 0, 0);
unsigned long lastMqttAttempt = 0;
// ─── Helpers ─────────────────────────────────────────────────────────────────
String buildTopic(uint32_t nodeId, const char *msgType) {
return String(T_VERSION) + "/" + T_TENANT + "/" + T_INDUSTRY + "/" + T_SITE +
"/" + T_GATEWAY + "/" + String(nodeId) + "/" + msgType;
}
IPAddress getStationIP() { return IPAddress(mesh.getStationIP()); }
// ─── MQTT callback (placeholder for future /cmd downlink) ────────────────────
void mqttCallback(char *topic, byte *payload, unsigned int len) {
Serial.printf("[mqtt] Received on %s: %.*s\n", topic, (int)len,
(char *)payload);
// Phase 2: parse /cmd and forward to specific mesh node via mesh.sendSingle()
}
// ─── Mesh callback: forward every message to the correct MQTT topic
// ───────────
void onMeshReceive(const uint32_t &from, const String &msg) {
Serial.printf("[mesh] From %u: %s\n", from, msg.c_str());
JsonDocument doc;
if (deserializeJson(doc, msg) != DeserializationError::Ok) {
Serial.println("[mesh] JSON parse failed");
return;
}
const char *msgType = doc["type"] | "data";
String topic = buildTopic(from, msgType);
if (mqttClient.connected()) {
mqttClient.publish(topic.c_str(), msg.c_str());
Serial.printf("[mqtt] Published → %s\n", topic.c_str());
} else {
Serial.println("[mqtt] Not connected — message dropped");
}
}
// ─── Setup ───────────────────────────────────────────────────────────────────
void setup() {