Overview
This project is your gateway to IoT data pipelines. The ESP32 reads a DHT22 sensor and publishes temperature and humidity to an MQTT broker every 30 seconds. You then subscribe to those topics in a Node-RED dashboard to see a live graph in your browser — no coding required on the dashboard side, just drag-and-drop nodes.
MQTT (Message Queuing Telemetry Transport) is the universal language of IoT devices. It is a lightweight publish-subscribe protocol designed for low-bandwidth, unreliable networks. Understanding it opens the door to Home Assistant, Node-RED, AWS IoT, Azure IoT Hub, and virtually every commercial IoT platform. This project gives you a solid foundation in MQTT topics, QoS levels, and broker architecture using real hardware.
By the end you will have a working IoT sensor node sending live data to a browser dashboard, and you will understand the core publish-subscribe pattern that powers billions of connected devices.
Components
- 1× ESP32 DevKit V1
- 1× DHT22 Temperature & Humidity Sensor
- 1× 10 kΩ Resistor — DHT22 pull-up
- 1× MQTT Broker (Mosquitto) — Install free on Raspberry Pi or PC: sudo apt install mosquitto
- 1× Node-RED — Free dashboard: sudo npm install -g node-red
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| DHT22 VCC | 3.3 V | |
| DHT22 DATA | GPIO 4 | 10 kΩ pull-up to 3.3 V |
| DHT22 GND | GND |
Arduino Code
/*
* ESP32 MQTT Sensor Dashboard — Beginner
* Publishes DHT22 temp & humidity to MQTT every 30 seconds.
* View live data in Node-RED, MQTT Explorer, or any subscriber.
*
* Setup:
* 1) Install Mosquitto broker on your network
* 2) Install Node-RED + node-red-dashboard
* 3) Import the Node-RED flow from the project page
* 4) Update SSID, PASSWORD, BROKER below
*
* Library: DHT (Adafruit), PubSubClient (Nick O'Leary)
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
const char* SSID = "YOUR_WIFI_SSID";
const char* PASSWORD = "YOUR_WIFI_PASSWORD";
const char* BROKER = "192.168.1.100"; // Your broker IP
const int PORT = 1883;
#define DHT_PIN 4
#define DHT_TYPE DHT22
// MQTT Topics (change "room1" to identify this sensor's location)
const char* TOPIC_TEMP = "esp32engine/sensors/room1/temperature";
const char* TOPIC_HUM = "esp32engine/sensors/room1/humidity";
const char* TOPIC_AVAIL= "esp32engine/sensors/room1/availability";
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient net;
PubSubClient mqtt(net);
void connectMQTT() {
while (!mqtt.connected()) {
Serial.print("Connecting MQTT... ");
// Last Will: publish "offline" if we disconnect unexpectedly
if (mqtt.connect("esp32-room1", "", "", TOPIC_AVAIL, 0, true, "offline")) {
mqtt.publish(TOPIC_AVAIL, "online", true);
Serial.println("connected");
} else {
Serial.printf("failed rc=%d — retry in 5sn", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
dht.begin();
WiFi.begin(SSID, PASSWORD);
Serial.print("Connecting Wi-Fi");
while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); }
Serial.printf("nConnected: %sn", WiFi.localIP().toString().c_str());
mqtt.setServer(BROKER, PORT);
connectMQTT();
}
void loop() {
if (!mqtt.connected()) connectMQTT();
mqtt.loop();
static unsigned long last = 0;
if (millis() - last >= 30000) {
last = millis();
float h = dht.readHumidity();
float t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) {
mqtt.publish(TOPIC_TEMP, String(t, 2).c_str(), true);
mqtt.publish(TOPIC_HUM, String(h, 2).c_str(), true);
Serial.printf("Published: temp=%.2f hum=%.2fn", t, h);
}
}
}How It Works
MQTT Publish-Subscribe: The ESP32 connects to the broker as a client and calls mqtt.publish(topic, value). Any other client subscribed to that topic receives the value immediately. The broker acts as a post office: senders drop messages in topic mailboxes, subscribers pick them up.
QoS 0 (At Most Once): mqtt.publish() uses QoS 0 by default: fire and forget. For sensor readings this is fine — if a packet is lost, the next reading arrives in 30 seconds anyway. For critical commands (turn off heater, open valve), use QoS 1 which guarantees delivery with acknowledgement.
Retained Messages: The third argument true to publish() sets the RETAIN flag. The broker stores the last retained message per topic and delivers it to any new subscriber instantly. Without retain, a Node-RED dashboard that reconnects after a restart would show no data until the next 30-second publish.
Last Will Testament: The LWT is registered during the MQTT CONNECT packet. If the ESP32's TCP connection drops (power failure, network loss), the broker automatically publishes "offline" to TOPIC_AVAIL. This lets dashboards show "Sensor offline" rather than showing stale old data as if it were current.
Applications
- Multi-room temperature monitoring displayed on a single dashboard
- Server room environmental monitoring with threshold alerts
- Greenhouse climate monitoring integrated with Home Assistant automations
- Historical temperature logging to understand heating/cooling efficiency
Troubleshooting
MQTT state is -2 (MQTT_CONNECT_FAILED)
Broker not reachable. Verify BROKER IP. Test with: mosquitto_pub -h BROKER_IP -t test -m hello from a terminal on the same network. Check that port 1883 is not blocked by firewall on the broker machine.
Node-RED dashboard shows old data and never updates
Ensure the MQTT in node in Node-RED is connected to the same broker. Check the topic exactly — "esp32engine/sensors/room1/temperature" must match character for character including case. Use MQTT Explorer (a free GUI tool) to see what topics are actually being published.
Upgrades
- Add a BMP280 pressure sensor on I2C to publish a third metric (air pressure)
- Add a PIR motion sensor and publish motion events to trigger Home Assistant automations
FAQ
You need an ESP32 DevKit, DHT22 DATA, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
At the intermediate level you expand to five sensors on one ESP32: DHT22, BMP280 (pressure + altitude), MQ-135 (air quality), a PIR motion sensor, and a photoresistor (light level). All readings publish to structured MQTT topics and are displayed on a Node-RED dashboard with gauges, charts, and alert nodes that send an email when CO2 levels are high.
You will learn JSON payload design for multi-metric MQTT messages, subscribing to MQTT topics to receive remote commands (toggle an LED from Node-RED), and structuring MQTT topic namespaces for scalable multi-device deployments.
Components
- 1× ESP32 DevKit V1
- 1× DHT22 — Temperature + humidity
- 1× BMP280 (I2C) — Barometric pressure + altitude
- 1× MQ-135 Gas Sensor — Air quality / CO2 proxy
- 1× PIR HC-SR501 — Motion detection
- 1× Photoresistor (LDR) + 10 kΩ — Light level (voltage divider)
- 1× LED + 220 Ω — Remote-controlled via MQTT command
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| DHT22 DATA | GPIO 4 | 10 kΩ pull-up |
| BMP280 SDA / SCL | GPIO 21 / 22 | I2C — shares bus with OLED if used |
| MQ-135 AOUT | GPIO 34 | Analogue output |
| PIR OUT | GPIO 27 | Digital HIGH on motion |
| LDR Voltage Divider | GPIO 35 | 10 kΩ + LDR to 3.3 V |
| LED + 220 Ω | GPIO 33 | Remotely toggled via MQTT |
Arduino Code
/*
* ESP32 MQTT Dashboard — Intermediate (5 sensors + remote control)
* Libraries: DHT (Adafruit), Adafruit_BMP280, PubSubClient
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <Wire.h>
#include <Adafruit_BMP280.h>
const char* SSID = "YOUR_WIFI_SSID";
const char* PASSWORD = "YOUR_WIFI_PASSWORD";
const char* BROKER = "192.168.1.100";
const char* DEV = "esp32-room1";
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define MQ_PIN 34
#define PIR_PIN 27
#define LDR_PIN 35
#define LED_PIN 33
const char* T_SENSOR = "esp32engine/sensors/room1/all";
const char* T_MOTION = "esp32engine/sensors/room1/motion";
const char* T_CMD = "esp32engine/sensors/room1/cmd";
const char* T_AVAIL = "esp32engine/sensors/room1/availability";
DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_BMP280 bmp;
WiFiClient net;
PubSubClient mqtt(net);
void onCmd(char* topic, byte* pl, unsigned int len){
String msg; for(unsigned int i=0;i<len;i++) msg+=(char)pl[i];
if(String(topic)==T_CMD){
if(msg=="LED_ON") digitalWrite(LED_PIN,HIGH);
if(msg=="LED_OFF") digitalWrite(LED_PIN,LOW);
if(msg=="RESTART") ESP.restart();
}
}
void connectMQTT(){
while(!mqtt.connected()){
if(mqtt.connect(DEV,"","",T_AVAIL,0,true,"offline")){
mqtt.publish(T_AVAIL,"online",true);
mqtt.subscribe(T_CMD);
} else delay(5000);
}
}
String buildJSON(float t,float h,float p,float alt,int mq,int ldr){
return "{"temperature":"+String(t,1)+","humidity":"+String(h,1)
+","pressure":"+String(p,1)+","altitude":"+String(alt,1)
+","air_quality":"+String(mq)+","light":"+String(ldr)+"}";
}
void setup(){
Serial.begin(115200);
dht.begin();
Wire.begin(); bmp.begin(0x76);
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
Adafruit_BMP280::SAMPLING_X2,Adafruit_BMP280::SAMPLING_X16,
Adafruit_BMP280::FILTER_X16,Adafruit_BMP280::STANDBY_MS_500);
pinMode(PIR_PIN,INPUT); pinMode(LED_PIN,OUTPUT);
analogReadResolution(12);
WiFi.begin(SSID,PASSWORD);
while(WiFi.status()!=WL_CONNECTED) delay(400);
mqtt.setServer(BROKER,1883);
mqtt.setCallback(onCmd);
connectMQTT();
}
void loop(){
if(!mqtt.connected()) connectMQTT();
mqtt.loop();
// Motion — publish immediately on change
static bool lastMotion=false;
bool motion=digitalRead(PIR_PIN);
if(motion!=lastMotion){
mqtt.publish(T_MOTION,motion?"detected":"clear",true);
lastMotion=motion;
}
// Sensor publish every 30 s
static unsigned long last=0;
if(millis()-last>=30000){
last=millis();
float t=dht.readTemperature(),h=dht.readHumidity();
float p=bmp.readPressure()/100.0f,alt=bmp.readAltitude(1013.25);
int mq=analogRead(MQ_PIN),ldr=analogRead(LDR_PIN);
if(!isnan(t)&&!isnan(h)){
String j=buildJSON(t,h,p,alt,mq,ldr);
mqtt.publish(T_SENSOR,j.c_str(),true);
Serial.println(j);
}
}
}How It Works
JSON Multi-Metric Payload: Instead of publishing each sensor value to a separate topic, this level bundles all readings into one JSON string on a single /all topic. Node-RED's JSON node parses the payload and routes each field to the appropriate dashboard widget using msg.payload.temperature etc. This reduces the number of MQTT messages by 5×.
Bidirectional MQTT: The ESP32 subscribes to T_CMD topic after connecting. When Node-RED sends "LED_ON" to that topic, onCmd() fires and drives the LED pin HIGH. This is the same pattern used by smart bulbs, smart plugs, and any remote-controllable IoT device.
Motion Event-Driven Publishing: Motion is published immediately when the PIR state changes rather than on a 30-second timer. This gives sub-second latency for motion alerts. The two publishing patterns (periodic for continuous sensors, event-driven for discrete sensors) are the two fundamental IoT data models.
Applications
- Field trial with visible OLED feedback
- Manual override for maintenance or testing
- Calibrated setup for daily use
Troubleshooting
BMP280 not found (address error)
Some BMP280 modules use I2C address 0x77 instead of 0x76. Try bmp.begin(0x77). Run an I2C scanner sketch to confirm the address.
MQ-135 readings are unstable for the first 30 minutes
MQ-series gas sensors require a 24-48 hour "burn-in" period and a 3-5 minute warm-up on every power cycle. Discard the first 5 minutes of readings after boot.
Upgrades
- Set up a Node-RED email node to send an alert when MQ-135 raw ADC exceeds a threshold
- Add a SIM800L GSM module for cellular backup when Wi-Fi is unavailable
FAQ
You need an ESP32 DevKit, DHT22 DATA, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The advanced level connects your ESP32 sensor data to a professional time-series stack: InfluxDB + Grafana. The ESP32 publishes JSON to MQTT. A Node-RED bridge subscribes to those topics and writes to an InfluxDB database. Grafana queries InfluxDB and renders a professional dashboard with multi-day history, statistical aggregations, and threshold alert annotations.
This is the same architecture used in real IoT production deployments. InfluxDB stores time-stamped sensor data with zero configuration degradation over years. Grafana gives you drag-and-drop dashboard building with dozens of panel types. You will understand the complete pipeline from hardware to database to visualisation.
Components
- 1× ESP32 DevKit V1 + sensors — Same as intermediate level
- 1× InfluxDB 2.x (free) — influxdata.com — local or cloud
- 1× Grafana (free) — grafana.com — local or cloud
- 1× Node-RED (bridge) — MQTT → InfluxDB bridge via influxdb node
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| Same as intermediate level | No new hardware | Software-only additions |
Arduino Code
/*
* ESP32 MQTT Dashboard — Advanced
* Same hardware as intermediate; adds OTA + structured JSON for InfluxDB.
* The Node-RED flow bridges MQTT → InfluxDB.
* No code changes needed on the ESP32 side — only Node-RED configuration.
*
* InfluxDB Node-RED flow setup:
* 1) Install: npm install node-red-contrib-influxdb
* 2) Subscribe to "esp32engine/sensors/+/all" (wildcard for all rooms)
* 3) JSON node → function node (add measurement name) → InfluxDB out node
*
* This sketch adds ArduinoOTA and a richer JSON payload with metadata.
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoOTA.h>
#include <DHT.h>
#include <Wire.h>
#include <Adafruit_BMP280.h>
#include <time.h>
const char* SSID = "YOUR_WIFI_SSID";
const char* PASSWORD = "YOUR_WIFI_PASSWORD";
const char* BROKER = "192.168.1.100";
const char* DEV_ID = "room1";
const char* LOCATION = "Living Room";
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define MQ_PIN 34
#define PIR_PIN 27
#define LDR_PIN 35
DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_BMP280 bmp;
WiFiClient net;
PubSubClient mqtt(net);
String topicAll = String("esp32engine/sensors/") + DEV_ID + "/all";
String topicAvail= String("esp32engine/sensors/") + DEV_ID + "/availability";
void connectMQTT(){
while(!mqtt.connected()){
if(mqtt.connect(("esp32-"+String(DEV_ID)).c_str(),"","",
topicAvail.c_str(),0,true,"offline")){
mqtt.publish(topicAvail.c_str(),"online",true);
} else delay(5000);
}
}
void setup(){
Serial.begin(115200);
dht.begin(); Wire.begin(); bmp.begin(0x76);
pinMode(PIR_PIN,INPUT);
analogReadResolution(12);
WiFi.begin(SSID,PASSWORD);
while(WiFi.status()!=WL_CONNECTED) delay(400);
configTime(0,0,"pool.ntp.org");
ArduinoOTA.setHostname(("esp32-"+String(DEV_ID)).c_str());
ArduinoOTA.begin();
mqtt.setServer(BROKER,1883);
connectMQTT();
}
void loop(){
ArduinoOTA.handle();
if(!mqtt.connected()) connectMQTT();
mqtt.loop();
static unsigned long last=0;
if(millis()-last>=30000){
last=millis();
float t=dht.readTemperature(),h=dht.readHumidity();
float p=bmp.readPressure()/100.0f;
char ts[25]=""; struct tm ti;
if(getLocalTime(&ti)) strftime(ts,sizeof(ts),"%Y-%m-%dT%H:%M:%SZ",&ti);
// Enriched JSON: add device_id, location, and ISO8601 timestamp
// InfluxDB line protocol can be generated in Node-RED from these fields
String j = "{"device":""+String(DEV_ID)+"","
""location":""+String(LOCATION)+"","
""temperature":"+String(t,2)+","
""humidity":"+String(h,2)+","
""pressure":"+String(p,1)+","
""air_quality":"+String(analogRead(MQ_PIN))+","
""light":"+String(analogRead(LDR_PIN))+","
""motion":"+String(digitalRead(PIR_PIN))+","
""ts":""+ts+""}";
mqtt.publish(topicAll.c_str(),j.c_str(),true);
Serial.println(j);
}
}How It Works
InfluxDB Time-Series Database: InfluxDB stores data as measurements (table name), fields (sensor values), tags (metadata like location), and a timestamp. Every row is automatically indexed by time, making range queries like "average temperature last 7 days" extremely fast. Unlike MySQL, InfluxDB is purpose-built for time-series data and needs no schema definition.
Node-RED Bridge: The MQTT-in node subscribes to all sensor topics using the esp32engine/sensors/+/all wildcard. A JSON node parses the payload. A function node maps fields to InfluxDB measurement fields and tags. The influxdb-out node writes the row. The entire bridge is 4 nodes with no programming.
Grafana Visualisation: Grafana connects to InfluxDB via a data source. Panels are configured with Flux queries (InfluxDB 2.x query language) — for example: from(bucket:"sensors") |> range(start:-24h) |> filter(fn:(r)=>r.device=="room1") |> filter(fn:(r)=>r._field=="temperature"). Grafana handles aggregation, downsampling, and alert rules natively.
Applications
- Remote monitoring from phone or laptop
- Automated alerts when limits are crossed
- Long-term trend logging for optimization
Troubleshooting
Grafana shows "No data" despite MQTT publishing
Verify Node-RED is receiving MQTT messages (debug node on the MQTT-in output). Check the InfluxDB data source in Grafana — test it with the built-in "Test" button. Confirm the bucket name and org name in the Flux query match your InfluxDB setup exactly.
Upgrades
- Add Grafana alerting to send a Telegram message when temperature exceeds threshold
- Set up InfluxDB data retention policies to downsample old data (1-min averages → 1-hour averages after 30 days)
FAQ
You need an ESP32 DevKit, DHT22 DATA, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.