ESP32 IoT Project

Real-Time Temperature & Humidity Monitoring with Remote Relay/LED Control using MQTT and a Cloud Dashboard

1. Project Overview

This tutorial walks through building a complete IoT monitoring and control system using an ESP32 microcontroller. The ESP32 connects to a Wi-Fi network, reads temperature and humidity data from a sensor, and publishes these readings to an MQTT broker connected to a cloud dashboard. The same dashboard is also used to send commands back to the ESP32 — for example, turning an LED or relay ON or OFF remotely.

Along the way, we will also fix a very common beginner mistake: MQTT publish throttling on free-tier cloud accounts, which can cause some sensor values (like humidity) to silently fail to appear on the dashboard.

What you will learn: Wi-Fi + MQTT connection handling, publishing multiple sensor values safely, subscribing to a control feed, and wiring a dashboard toggle switch to a physical relay/LED.

2. System Architecture

The system has two directions of data flow: uplink (sensor → dashboard) and downlink (dashboard → actuator). Both directions pass through the same MQTT broker.

DHT Sensor
(Temp & Humidity)
ESP32
MQTT Broker
(Cloud)
Dashboard
(Feeds/Charts)
Uplink: Sensor data is published from the ESP32 to the broker, which forwards it to the dashboard feeds.
Dashboard
(Toggle Switch)
MQTT Broker
(Cloud)
ESP32
Relay / LED
Downlink: The dashboard publishes a command that the ESP32 has subscribed to, triggering a digital output.

3. Hardware Components

ComponentPurpose
ESP32 Development BoardMain microcontroller with built-in Wi-Fi
DHT11 / DHT22 SensorReads temperature and humidity
Relay Module or LEDActuator controlled remotely via the dashboard
Onboard LED (GPIO 2)Used for a simple demo without extra wiring
Jumper Wires + BreadboardConnections between sensor, relay, and ESP32

4. Connecting the ESP32 to Wi-Fi

The very first step in any ESP32 IoT project is establishing a stable Wi-Fi connection. Once connected, the board is assigned a local IP address, which confirms it can now reach the internet and, subsequently, the MQTT broker.

#include <WiFi.h>

const char* ssid     = "RoboticsLab";
const char* password = "your_wifi_password";

void connectWiFi() {
  Serial.print("Connecting to Wi-Fi");
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("\nWi-Fi connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
}
Once you see an IP address printed in the Serial Monitor, the ESP32 has successfully joined the network and is ready to attempt the MQTT connection.

5. Connecting to the MQTT Broker

MQTT (Message Queuing Telemetry Transport) is a lightweight publish/subscribe protocol ideal for IoT devices. After Wi-Fi is established, the ESP32 must connect to the broker using a client ID, username, and key/password.

#include <PubSubClient.h>

const char* mqtt_server = "io.adafruit.com";
const int   mqtt_port   = 1883;
const char* mqtt_user   = "YOUR_USERNAME";
const char* mqtt_key    = "YOUR_AIO_KEY";

WiFiClient espClient;
PubSubClient client(espClient);

void connectMQTT() {
  while (!client.connected()) {
    Serial.println("Connecting to MQTT...");
    if (client.connect("ESP32Client", mqtt_user, mqtt_key)) {
      Serial.println("MQTT connected!");
      client.subscribe("YOUR_USERNAME/feeds/led");
    } else {
      Serial.print("Failed, rc=");
      Serial.print(client.state());
      Serial.println(" retrying in 5 seconds");
      delay(5000);
    }
  }
}

In the Serial Monitor, this typically appears as a sequence of connection status messages, for example:

  1. Connecting to Wi-Fi
  2. Wi-Fi connected
  3. Connecting to MQTT
  4. MQTT connection failed, retrying
  5. MQTT connected

6. Publishing Temperature & Humidity Data

Once connected, the ESP32 reads values from the DHT sensor and publishes them to two separate feeds — one for temperature and one for humidity.

#include <DHT.h>

#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

void publishSensorData() {
  float temperature = dht.readTemperature();
  float humidity    = dht.readHumidity();

  char tempStr[8];
  char humStr[8];
  dtostrf(temperature, 4, 2, tempStr);
  dtostrf(humidity, 4, 2, humStr);

  client.publish("YOUR_USERNAME/feeds/temperature", tempStr);
  client.publish("YOUR_USERNAME/feeds/humidity", humStr);
}
Common Symptom: Temperature appears correctly on the dashboard, but humidity does not update — even though both values print correctly in the Serial Monitor and both feeds are configured correctly on the dashboard.

7. The Hidden Problem: MQTT Publish Throttling

Many free-tier MQTT/cloud dashboard services (such as Adafruit IO's free plan) enforce a rate limit on how many messages can be published per second. When two client.publish() calls are sent back-to-back with no delay, the broker may silently drop the second message. This is exactly why temperature (published first) succeeds, while humidity (published immediately after) fails to reach the dashboard.

The Fix: Add a Short Delay Between Publishes

void publishSensorData() {
  float temperature = dht.readTemperature();
  float humidity    = dht.readHumidity();

  char tempStr[8];
  char humStr[8];
  dtostrf(temperature, 4, 2, tempStr);
  dtostrf(humidity, 4, 2, humStr);

  client.publish("YOUR_USERNAME/feeds/temperature", tempStr);
  delay(3000);   // wait 3 seconds to avoid rate-limit throttling
  client.publish("YOUR_USERNAME/feeds/humidity", humStr);
}
Best Practice: On paid or self-hosted MQTT brokers, this delay can usually be reduced or removed. On free-tier cloud services, a 1–3 second gap between successive publishes is a safe, simple workaround.

8. Remote Relay / LED Control (Subscribing to Commands)

To control a device remotely, the ESP32 must subscribe to a feed (e.g. led) and define a callback function that runs whenever a new message arrives on that feed.

#define RELAY_PIN 2   // onboard LED or external relay

void callback(char* topic, byte* payload, unsigned int length) {
  String message;
  for (unsigned int i = 0; i < length; i++) {
    message += (char)payload[i];
  }

  Serial.print("Message on [");
  Serial.print(topic);
  Serial.print("]: ");
  Serial.println(message);

  if (message == "ON") {
    digitalWrite(RELAY_PIN, HIGH);
  } else if (message == "OFF") {
    digitalWrite(RELAY_PIN, LOW);
  }
}

void setup() {
  pinMode(RELAY_PIN, OUTPUT);
  client.setCallback(callback);
}

The reaction time between toggling the switch on the dashboard and the physical LED/relay switching state is typically near-instantaneous, since MQTT delivers messages in real time rather than through slow polling.

Adding a Second Controllable Feed

The same pattern can be repeated to add more controllable outputs. For example, adding a second relay output on a different pin only requires:

  1. Define a new output pin, e.g. #define RELAY2_PIN 5
  2. Set it as OUTPUT in setup()
  3. Subscribe to a new feed, e.g. YOUR_USERNAME/feeds/relay2
  4. Check the topic name inside the callback() function
  5. Call digitalWrite(RELAY2_PIN, HIGH/LOW) based on the payload

9. Setting Up the Cloud Dashboard

The dashboard (e.g. Adafruit IO) displays incoming sensor data and provides interactive widgets to send commands back to the device. Below are the steps to configure it:

Creating Feeds

  1. Log in to your MQTT dashboard account
  2. Click "New Feed"
  3. Name the feed "temperature"
  4. Repeat and create a feed named "humidity"
  5. Repeat and create a feed named "led" (for the relay control)

Building the Dashboard Layout

  1. Create a new Dashboard
  2. Add a "Gauge" or "Line Chart" block for temperature
  3. Add a "Gauge" or "Line Chart" block for humidity
  4. Add a "Toggle Switch" block
  5. Link the toggle switch to the "led" feed
  6. Set the switch's ON value to "ON" and OFF value to "OFF"
  7. Save the layout
After uploading the sketch, refresh the dashboard page. Both the temperature and humidity widgets should now update with live values, and toggling the switch should immediately turn the relay/LED on or off.

10. Complete Example Sketch

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>

#define DHTPIN 4
#define DHTTYPE DHT22
#define RELAY_PIN 2

const char* ssid       = "RoboticsLab";
const char* password   = "your_wifi_password";
const char* mqtt_server= "io.adafruit.com";
const char* mqtt_user  = "YOUR_USERNAME";
const char* mqtt_key   = "YOUR_AIO_KEY";

WiFiClient espClient;
PubSubClient client(espClient);
DHT dht(DHTPIN, DHTTYPE);

void callback(char* topic, byte* payload, unsigned int length) {
  String message;
  for (unsigned int i = 0; i < length; i++) message += (char)payload[i];
  if (message == "ON")  digitalWrite(RELAY_PIN, HIGH);
  if (message == "OFF") digitalWrite(RELAY_PIN, LOW);
}

void connectWiFi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
}

void connectMQTT() {
  while (!client.connected()) {
    if (client.connect("ESP32Client", mqtt_user, mqtt_key)) {
      client.subscribe("YOUR_USERNAME/feeds/led");
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_PIN, OUTPUT);
  dht.begin();
  connectWiFi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void loop() {
  if (!client.connected()) connectMQTT();
  client.loop();

  float temperature = dht.readTemperature();
  float humidity    = dht.readHumidity();

  char tempStr[8];
  char humStr[8];
  dtostrf(temperature, 4, 2, tempStr);
  dtostrf(humidity, 4, 2, humStr);

  client.publish("YOUR_USERNAME/feeds/temperature", tempStr);
  delay(3000);
  client.publish("YOUR_USERNAME/feeds/humidity", humStr);
  delay(1500);
}

11. Key Takeaways & Best Practices