Build Your First Real IoT Device: ESP32 & MQTT Beginner’s Guide

Build Your First Real IoT Device: ESP32 & MQTT Beginner’s Guide

Are you still using HTTP requests to control your IoT devices? If your ESP32 is constantly asking the server "Is there any data for me?" (polling), you might be draining your battery and wasting bandwidth.

In this tutorial, we are going to upgrade your skills by switching to MQTT (Message Queuing Telemetry Transport). We will build a system where an ESP32 sends sensor data to a dashboard and listens for commands to control an LED instantly.

We will also implement a professional IoT feature called "Last Will and Testament" (LWT) to instantly detect if our device goes offline.

Why MQTT? (Vs. HTTP)

Before we wire anything up, it’s important to understand why we are using MQTT.

  • HTTP (The Old Way): Think of this like checking your physical mailbox every 5 minutes to see if you have a letter. It’s inefficient and tiring.
  • MQTT (The IoT Way): Think of this like a WhatsApp notification. You don't check for messages; the message arrives instantly when it's sent.

MQTT uses a Publisher/Subscriber model. The ESP32 doesn't need to know who is listening; it just "publishes" data to a specific topic, and anyone "subscribed" to that topic receives it.

MQTT Broker & 4 clients

What We Are Building

  1. Publisher: The ESP32 will send data (simulated temperature, WiFi signal strength, and uptime) to the broker every 5 seconds.
  2. Subscriber: The ESP32 will listen for "ON" or "OFF" commands to toggle an LED.
  3. Last Will: If the ESP32 is unplugged, the system will automatically alert us that the device is "Offline."

Watch the Video Tutorial

If you prefer learning visually, I have put together a complete step-by-step video walkthrough of this project. In the video, I demonstrate the hardware setup, explain the code logic in detail, and show the live bi-directional communication between the ESP32 and the MQTT broker.

Hardware Required

  • ESP32 Development Board
  • 1x LED (Any color)
  • 1x 330Ω Resistor
  • Breadboard and Jumper Wires
  • Micro USB Cable

Software Tools

  • Arduino IDE (for coding the ESP32)
  • MQTTX (A free desktop software to visualize data)

Step 1: The Circuit

The wiring is very straightforward. We are using the ESP32's GPIO capabilities.

  • LED Anode (+): Connect to GPIO 27
  • LED Cathode (-): Connect to GND via the 330Ω resistor.
Note: If you are using the external LED, ensure the resistor is in series to prevent burning out the LED.
Circuit Diagram

Step 2: The Code Logic

We will be using the PubSubClient library for MQTT and ArduinoJson to format our data nicely.

Key Concepts in the Code:

  1. Topic Structure: We organize our data using a path format: DeviceType/DeviceID/Function.
    • Data Topic: ESP32/Unit1/data
    • Command Topic: ESP32/Unit1/command
    • Status Topic: ESP32/Unit1/status
  2. Non-Blocking Loop: Instead of delay(5000), which pauses the brain of the ESP32, we use a timer check. This allows the ESP32 to receive LED commands while it waits to send the next temperature reading.
  3. The "Last Will": During the connection setup, we tell the broker: "If I disconnect unexpectedly, please publish the message 'Offline' to my status topic."

Github

IoT_Bhai_Youtube_Channel/Mastering ESP32(English)/5. How to Use MQTT with ESP32 Step by Step Tutorial (Code & Experiment)/How_to_Use_MQTT_with_ESP32_Step_by_Step/How_to_Use_MQTT_with_ESP32_Step_by_Step.ino at main · ittipu/IoT_Bhai_Youtube_Channel
Contribute to ittipu/IoT_Bhai_Youtube_Channel development by creating an account on GitHub.

Github Link

/*
 * PROFESSIONAL MQTT EXPERIMENT - ESP32
 * * Features:
 * - Non-blocking Architecture (No delay())
 * - Automatic Reconnection (WiFi & MQTT)
 * - LWT (Last Will & Testament) for State Monitoring
 * - JSON Data Serialization
 * - Remote Command Handling
 */

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

// ==========================================
// 1. CONFIGURATION (Edit these)
// ==========================================
const char* ssid = "";
const char* password = "";

// MQTT Broker Settings (Using public HiveMQ for demo, change for production)
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883; 
const char* mqtt_user = ""; // Leave blank for public brokers
const char* mqtt_pass = "";

// Unique Device ID (Must be unique on the broker)
const char* device_id = "ESP32_Pro_Unit_01"; 

// Topics (Structure: device_type/device_id/function)
const char* topic_telemetry = "esp32/unit01/data";   // Where we send sensor data
const char* topic_command   = "esp32/unit01/cmd";    // Where we listen for commands
const char* topic_status    = "esp32/unit01/status"; // LWT (Online/Offline)

// ==========================================
// 2. GLOBAL OBJECTS & VARIABLES
// ==========================================
WiFiClient espClient;
PubSubClient client(espClient);

// Timers for non-blocking delays
unsigned long lastMsgTime = 0;
const long interval = 5000; // Send data every 5 seconds

#define LED_PIN 2 // Onboard LED

// ==========================================
// 3. SETUP WIFI
// ==========================================
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to WiFi: ");
  Serial.println(ssid);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

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

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// ==========================================
// 4. CALLBACK (Handle Incoming Messages)
// ==========================================
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");

  // Convert payload to string for easier handling
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);

  // -- Command Logic --
  // Example: If we receive "ON", turn on LED
  if (String(topic) == topic_command) {
    if (message == "ON") {
      digitalWrite(LED_PIN, HIGH);
      // Feedback: Publish new state immediately
      client.publish(topic_telemetry, "{\"led\": \"ON\"}"); 
    } else if (message == "OFF") {
      digitalWrite(LED_PIN, LOW);
      client.publish(topic_telemetry, "{\"led\": \"OFF\"}");
    }
  }
}

// ==========================================
// 5. RECONNECT (The Engine Room)
// ==========================================
void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    
    // --- LWT CONFIGURATION ---
    // define Last Will: Topic, QoS, Retain, Message
    // If this ESP32 dies, the Broker will post "offline" to the status topic automatically.
    
    if (client.connect(device_id, mqtt_user, mqtt_pass, topic_status, 1, true, "offline")) {
      Serial.println("connected");
      
      // Once connected, publish an announcement that we are alive (Retained = true)
      client.publish(topic_status, "online", true);
      
      // Resubscribe to command topics
      client.subscribe(topic_command);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000); // Blocking delay here is acceptable as we can't operate without connection
    }
  }
}

// ==========================================
// 6. MAIN SETUP
// ==========================================
void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  
  setup_wifi();
  
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

// ==========================================
// 7. MAIN LOOP
// ==========================================
void loop() {
  // Ensure we stay connected
  if (!client.connected()) {
    reconnect();
  }
  client.loop(); // Keep MQTT alive

  // --- Non-Blocking Timer for Telemetry ---
  unsigned long now = millis();
  if (now - lastMsgTime > interval) {
    lastMsgTime = now;
    
    // Create a JSON Document
    JsonDocument doc; // ArduinoJson v7
    doc["device"] = device_id;
    doc["uptime"] = millis() / 1000;
    doc["wifi_rssi"] = WiFi.RSSI();
    
    // Add dynamic data (simulated sensor)
    doc["temp"] = random(20, 30); 

    // Serialize JSON to String
    char buffer[256];
    serializeJson(doc, buffer);

    // Publish to MQTT
    Serial.print("Publishing data: ");
    Serial.println(buffer);
    client.publish(topic_telemetry, buffer);
  }
}

Code

Step 3: Visualization with MQTTX

Now that the code is running, we need a way to see the data and control the LED. We will use MQTTX.

  1. Connect: Open MQTTX and create a new connection using the same Broker URL (broker.hivemq.com) and Port (1883).
  1. Subscribe: Click "New Subscription" and enter ESP32/unit01/data. You should immediately see JSON data arriving every 5 seconds:
  2. Control: In the text input area, change the topic to ESP32/unit01/cmd. Type on and hit send.
  • Result: The LED on your ESP32 should light up instantly!
LED ON

The "Killer Feature": Testing the Last Will

This is the coolest part of the experiment.

  1. Subscribe to the topic ESP32/Unit01/status in MQTTX.
  2. You should see the message "Online".
  3. Now, physically unplug the USB cable from your ESP32.
  4. Watch MQTTX. Even though the device is dead, the Broker will automatically publish "Offline" to the status topic.
device status (LWT)

This is how professional IoT dashboards know when a sensor has run out of battery or lost connection.

Conclusion

You have successfully moved beyond simple HTTP requests and built a responsive, bi-directional IoT device using MQTT.

We covered:

  • The efficiency of the Push vs. Pull model.
  • Formatting data with JSON.
  • Using "Last Will" for connection health checks.