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() {
Serial.begin(115200);
delay(100);
// 1. Mesh — setRoot() and stationManual() BEFORE init()
// stationManual() hands STA management to the mesh library:
// it will NOT disconnect from the router to scan for mesh peers.
mesh.setDebugMsgTypes(ERROR | CONNECTION);
mesh.setRoot(true);
mesh.setContainsRoot(true);
mesh.init(MESH_PREFIX, MESH_PASSWORD, MESH_PORT, WIFI_AP_STA); // auto channel
mesh.onReceive(&onMeshReceive);
mesh.onNewConnection(
[](size_t id) { Serial.printf("[mesh] +Node %u\n", (uint32_t)id); });
mesh.onDroppedConnection(
[](size_t id) { Serial.printf("[mesh] -Node %u\n", (uint32_t)id); });
mesh.stationManual(STATION_SSID,
STATION_PASSWORD); // ← key: mesh owns the STA
mesh.setHostname("ais-gateway");
// 2. TLS client — setInsecure() skips cert check (ok for dev)
wifiClient.setInsecure();
wifiClient.setTimeout(5);
mqttClient.setServer(MQTT_HOST, MQTT_PORT_TLS);
mqttClient.setCallback(mqttCallback);
mqttClient.setKeepAlive(30);
mqttClient.setSocketTimeout(5);
mqttClient.setBufferSize(512);
Serial.println("[gw] Gateway started — waiting for IP...");
}
// ─── Loop ────────────────────────────────────────────────────────────────────
void loop() {
mesh.update(); // must run every iteration
mqttClient.loop(); // process keep-alive / incoming
// Connect (or reconnect) to MQTT whenever the station IP changes.
// This is the official painlessMesh bridge pattern — no blocking delay().
IPAddress ip = getStationIP();
if (ip != myIP) {
myIP = ip;
Serial.printf("[wifi] IP: %s\n", myIP.toString().c_str());
if (myIP != IPAddress(0, 0, 0, 0)) {
Serial.print("[mqtt] Connecting... ");
String clientId = "gw-" + String(ESP.getChipId(), HEX);
if (mqttClient.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) {
Serial.println("OK");
// Publish gateway online status
String statusTopic = String(T_VERSION) + "/" + T_TENANT + "/" +
T_INDUSTRY + "/" + T_SITE + "/" + T_GATEWAY +
"/status";
mqttClient.publish(statusTopic.c_str(), "{\"state\":\"online\"}");
} else {
Serial.printf("[mqtt] Failed (rc=%d)\n", mqttClient.state());
}
}
}
// Non-blocking reconnect if MQTT dropped after initial connect
if (myIP != IPAddress(0, 0, 0, 0) && !mqttClient.connected()) {
unsigned long now = millis();
if (now - lastMqttAttempt > 5000) {
lastMqttAttempt = now;
Serial.print("[mqtt] Reconnecting... ");
String clientId = "gw-" + String(ESP.getChipId(), HEX);
if (mqttClient.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) {
Serial.println("OK");
} else {
Serial.printf("[mqtt] Failed (rc=%d)\n", mqttClient.state());
}
}
}
}
Conclusion
Though this is very complex in terms of code. It is very efficient in terms of power and data consumption.
If you have any questions or suggestions, please feel free to ask in the comments section.
I will be going indetail how these both sender and receiver works in my next article.
Any feedback or contributors are welcome! It’s online, source-available, and ready for anyone to use.
⭐ Star it on GitHub: https://github.com/HexmosTech/git-lrc


Top comments (0)