
Ganesh KumarHello, I'm Ganesh. I'm building git-lrc, an AI code reviewer that runs on every commit. It is free,...
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 with a NodeMCUv2 (ESP8266) board and send data from one ESP8266 board to another ESP8266 board.
Previously we explored how to use MQTT with ESP8266.
Now let's integrate painlessmesh with MQTT.
Let's assume we have 2 ESP8266 boards.
One is gateway and another is node.
We want to send data from node to gateway using painlessmesh. Next we want to send data from gateway to cloud using MQTT.
For example we will use DHT11 sensor and mq series gas sensor with node to send data to gateway.
This operation should be done with very minimal data transfer.
If one node has 2 or more sensors it should be compressed and sent to gateway.
Painlessmesh is very heavy library. It does lot of operations to keep network alive. With this if we send data without compressing it. It will consume lot of power and data. Casuing network to be unstable.
Main Goal is
I will be combining all the above mentioned in single project.
In this article we will be covering first 2 points.
For this example we will use DHT11 sensor and MQ2 gas sensor.
#define MESH_PREFIX "secreat_name"
#define MESH_PASSWORD "SecreateKey"
#define MESH_PORT 5555
With this funtion we create scheduler and painlessmesh object.
#include <painlessMesh.h>
mesh.init(MESH_PREFIX, MESH_PASSWORD, &scheduler, MESH_PORT);
mesh.onReceive(&onReceive);
mesh.onNewConnection(&onNewConnection);
We use DHT library sensor to measure temperature and humidity.
We use D2 pin to connect DHT11 sensor.
#include <DHT.h>
#define DHT_PIN D2
#define DHT_TYPE DHT11
DHT dht(DHT_PIN, DHT_TYPE);
Read the temperature and humidity from the sensor.
float t = dht.readTemperature();
float h = dht.readHumidity();
As Dht sensor should have 1 second delay between readings. We will use scheduler to read the temperature and humidity from the sensor.
Task readDHTTask(10000, TASK_FOREVER, []() {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (isnan(t) || isnan(h)) {
Serial.println("Failed to read from DHT sensor!");
return;
}
Serial.printf("Temperature: %.2f C, Humidity: %.2f %%", t, h);
});
We use MQ2 gas sensor to measure the gas concentration.
We use A0 pin to connect MQ2 gas sensor.
#define MQ2_PIN A0
Read the gas concentration from the sensor.
float gas = analogRead(MQ2_PIN);
As MQ2 sensor should have 1 second delay between readings. We will use scheduler to read the gas concentration from the sensor.
Task readMQ2Task(10000, TASK_FOREVER, []() {
float gas = analogRead(MQ2_PIN);
if (isnan(gas)) {
Serial.println("Failed to read from MQ2 sensor!");
return;
}
Serial.printf("Gas: %.2f ppm", gas);
});
We use bit packing to compress the data.
As sensor will be in specified range we can use bit packing to compress the data.
String packToHex(float temp, float hum, float gas) {
uint16_t packedTemp = (uint16_t)(temp * 10.0f) & 0x1FF; // 9 bits
uint16_t packedHum = (uint16_t)((hum - 20.0f) * 10.0f) & 0x3FF; // 10 bits
uint16_t packedGas = (uint16_t)(gas / 10.0f) & 0x3FF; // 10 bits
uint32_t packed = 0;
packed |= (uint32_t)packedTemp << 20; // bits 28-20
packed |= (uint32_t)packedHum << 10; // bits 19-10
packed |= (uint32_t)packedGas; // bits 9-0
char buf[9];
snprintf(buf, sizeof(buf), "%08X", packed);
return String(buf);
}
Finaly commbining all these into one code.
/**
* sensor_node_1.cpp — PainlessMesh Sensor Node (DHT11 + MQ6)
*
* Boot flow:
* 1. Joins the mesh network
* 2. On first connection to gateway → broadcasts /config (packing schema)
* The gateway forwards /config to MQTT → backend caches the schema
* 3. After 3 s delay → starts sending /data every 5 s
* (gives backend time to process /config before first hex payload arrives)
*
* /alert is sent immediately any time gas > GAS_THRESHOLD.
*
* Bit packing layout (29 bits used, 3 MSBs unused):
* bits 28-20 (9 bits) : temp = temp_celsius * 10 (0–511 → 0–51.1°C)
* bits 19-10 (10 bits): hum = (humidity - 20) * 10 (0–1023 → 20–122.3%)
* bits 9-0 (10 bits): gas = gas_ppm / 10 (0–1023 → 0–10230
* ppm)
*/
#include <Arduino.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <painlessMesh.h>
// ─── Mesh credentials (must match gateway) ───────────────────────────────────
#define MESH_PREFIX "secreat_name"
#define MESH_PASSWORD "SecreateKey"
#define MESH_PORT 5555
// ─── Sensor pins ─────────────────────────────────────────────────────────────
#define DHT_PIN D2
#define DHT_TYPE DHT11
#define MQ6_PIN A0 // analog pin
// ─── Alert threshold ─────────────────────────────────────────────────────────
#define GAS_THRESHOLD 5000 // ppm — triggers /alert
// ─── Timing ──────────────────────────────────────────────────────────────────
#define DATA_INTERVAL_MS 5000
// ─── Globals ─────────────────────────────────────────────────────────────────
Scheduler scheduler;
painlessMesh mesh;
DHT dht(DHT_PIN, DHT_TYPE);
bool configSent = false; // send /config once per gateway connection
// ─── Bit packing ─────────────────────────────────────────────────────────────
/**
* Pack temperature, humidity and gas into an 8-char hex string.
* The encoding matches what the backend's unpackHex() expects.
*
* temp → 9 bits, mult 10, min 0
* hum → 10 bits, mult 10, min 20
* gas → 10 bits, div 10, min 0
*/
String packToHex(float temp, float hum, float gas) {
uint16_t packedTemp = (uint16_t)(temp * 10.0f) & 0x1FF; // 9 bits
uint16_t packedHum = (uint16_t)((hum - 20.0f) * 10.0f) & 0x3FF; // 10 bits
uint16_t packedGas = (uint16_t)(gas / 10.0f) & 0x3FF; // 10 bits
uint32_t packed = 0;
packed |= (uint32_t)packedTemp << 20; // bits 28-20
packed |= (uint32_t)packedHum << 10; // bits 19-10
packed |= (uint32_t)packedGas; // bits 9-0
char buf[9];
snprintf(buf, sizeof(buf), "%08X", packed);
return String(buf);
}
// ─── Message builders
// ─────────────────────────────────────────────────────────
/**
* Build the /config JSON that tells the backend how to decode our hex payloads.
* This matches the NodeConfig struct on the backend.
*/
String buildConfigMsg() {
JsonDocument doc;
doc["type"] = "config";
doc["hw"] = "NodeMCUv2";
JsonObject packing = doc["packing"].to<JsonObject>();
packing["bits"] = 32;
JsonArray layout = packing["layout"].to<JsonArray>();
layout.add("temp");
layout.add("hum");
layout.add("gas");
JsonObject sensors = doc["sensors"].to<JsonObject>();
JsonObject t = sensors["temp"].to<JsonObject>();
t["model"] = "DHT11";
t["metric"] = "temperature";
t["min"] = 0;
t["max"] = 50;
t["mult"] = 10;
t["bits"] = 9;
JsonObject h = sensors["hum"].to<JsonObject>();
h["model"] = "DHT11";
h["metric"] = "humidity";
h["min"] = 20;
h["max"] = 90;
h["mult"] = 10;
h["bits"] = 10;
JsonObject g = sensors["gas"].to<JsonObject>();
g["model"] = "MQ6";
g["metric"] = "gas_mq6";
g["min"] = 0;
g["max"] = 10000;
g["div"] = 10;
g["bits"] = 10;
String out;
serializeJson(doc, out);
return out;
}
String buildDataMsg(const String &hexStr) {
JsonDocument doc;
doc["type"] = "data";
doc["d"] = hexStr;
String out;
serializeJson(doc, out);
return out;
}
String buildAlertMsg(const String &hexStr, const char *cause) {
JsonDocument doc;
doc["type"] = "alert";
doc["cause"] = cause;
doc["d"] = hexStr;
String out;
serializeJson(doc, out);
return out;
}
// ─── Periodic data task
// ────────────────────────────────────────────────────────
Task dataTask(DATA_INTERVAL_MS, TASK_FOREVER, []() {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
// MQ6: raw analog 0-1023 → scale to ppm (rough calibration, adjust to yours)
int raw = analogRead(MQ6_PIN);
float gas = raw * (10000.0f / 1023.0f); // map 0-1023 → 0-10000 ppm
if (isnan(temp) || isnan(hum)) {
Serial.println("[sensor] DHT read failed");
return;
}
Serial.printf("[sensor] temp=%.1f°C hum=%.1f%% gas=%.0f ppm\n", temp, hum,
gas);
String hexStr = packToHex(temp, hum, gas);
// Always send /data
mesh.sendBroadcast(buildDataMsg(hexStr));
// Send /alert if threshold breached
if (gas > GAS_THRESHOLD) {
Serial.println("[sensor] ⚠ Gas threshold breached — sending alert");
mesh.sendBroadcast(buildAlertMsg(hexStr, "gas_mq6"));
}
});
// One-shot task: fires 3 s after /config is sent, then enables dataTask.
// The 3 s gives the gateway time to publish /config to MQTT and the backend
// time to cache the packing schema before the first hex payload arrives.
Task startupTask(3000, 1, []() {
Serial.println("[sensor] Config settle time elapsed — starting data stream");
scheduler.addTask(dataTask);
dataTask.enable();
});
// ─── Mesh callbacks
// ────────────────────────────────────────────────────────────
void onReceive(uint32_t from, String &msg) {
Serial.printf("[mesh] From %u: %s\n", from, msg.c_str());
// Future: handle /cmd messages from gateway
}
void onNewConnection(size_t nodeId) {
Serial.printf("[mesh] +Connected to %u\n", (uint32_t)nodeId);
if (!configSent) {
// First connection — push /config so backend can cache packing schema
mesh.sendBroadcast(buildConfigMsg());
Serial.println("[mesh] Sent /config — waiting 3 s before data stream");
configSent = true;
// Start the 3-second countdown before data begins
scheduler.addTask(startupTask);
startupTask.enable();
}
}
// ─── Setup / Loop
// ─────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(100);
dht.begin();
mesh.setDebugMsgTypes(ERROR | CONNECTION);
mesh.setContainsRoot(
true); // tells node the root (gateway) is in this network
mesh.init(MESH_PREFIX, MESH_PASSWORD, &scheduler, MESH_PORT);
mesh.onReceive(&onReceive);
mesh.onNewConnection(&onNewConnection);
Serial.printf("[mesh] Node ID: %u — waiting for gateway connection...\n",
mesh.getNodeId());
}
void loop() { mesh.update(); }
By above code we can send data from multiple sensors to gateway using painlessmesh.
In this article we have seen how to use painlessmesh with a NodeMCUv2 (ESP8266) board and send data from one ESP8266 board to another ESP8266 board.
In next article we will integrate scallable gateway.
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