DEV Community

Cover image for How to send sensor data to a gateway using PainlessMesh in ESP8266?
Ganesh Kumar
Ganesh Kumar

Posted on

How to send sensor data to a gateway using PainlessMesh in ESP8266?

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.

What Problem we are solving?

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.

What We will be achieving?

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

  1. Receive data from multiple sensors
  2. Compress the data
  3. Send the data to gateway
  4. Publish the data from received at gateway to cloud using MQTT
  5. Scallable MQTT topics publishing

I will be combining all the above mentioned in single project.

In this article we will be covering first 2 points.

  1. Receiving Data from Multiple Sensors
  2. Compressing the data
  3. Send the data to gateway

Receiving Data from Multiple Sensors and Compressing it

For this example we will use DHT11 sensor and MQ2 gas sensor.

  1. Setup connection with painlessmesh We will use these credentials for painlessmesh.
#define MESH_PREFIX "secreat_name"
#define MESH_PASSWORD "SecreateKey"
#define MESH_PORT 5555

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
  1. Setup connection with DHT11 sensor

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);
Enter fullscreen mode Exit fullscreen mode

Read the temperature and humidity from the sensor.

float t = dht.readTemperature();
float h = dht.readHumidity();
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode
  1. Setup connection with MQ2 gas sensor

We use MQ2 gas sensor to measure the gas concentration.

We use A0 pin to connect MQ2 gas sensor.

#define MQ2_PIN A0
Enter fullscreen mode Exit fullscreen mode

Read the gas concentration from the sensor.

float gas = analogRead(MQ2_PIN);
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode
  1. Compress the data with bit packing

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);
}
Enter fullscreen mode Exit fullscreen mode

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(); }
Enter fullscreen mode Exit fullscreen mode

By above code we can send data from multiple sensors to gateway using painlessmesh.

Conclution

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.

git-lrc

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)