28  MQTT Hands-On Labs and Exercises

In 60 Seconds

These MQTT labs provide complete, runnable code for building IoT systems: ESP32 temperature/humidity publishers with configurable QoS, real-time Python dashboards for data visualization, multi-device home automation with motion-triggered lighting, and systematic debugging exercises for common MQTT connection and delivery issues.

28.1 Learning Objectives

By the end of these labs, you will be able to:

  • Implement ESP32 MQTT Publishers: Configure and deploy temperature/humidity sensors publishing sensor data to MQTT topics with appropriate QoS levels
  • Construct Python MQTT Dashboards: Build real-time data visualization subscribers that aggregate and display multi-sensor data streams
  • Design Multi-Device Home Automation: Architect and develop MQTT-based publish/subscribe communication between motion sensors and actuators
  • Diagnose MQTT Connection Failures: Systematically analyze and resolve common connection, subscription, and message delivery problems using structured debugging techniques
  • Evaluate QoS Trade-offs: Compare the energy, bandwidth, and reliability implications of QoS 0, 1, and 2 to justify the appropriate level for a given IoT application
  • Calculate MQTT Bandwidth and Energy Costs: Apply formulas to estimate network throughput and battery life for sensor fleets under different QoS and publish-interval configurations
  • MQTT: Message Queuing Telemetry Transport — pub/sub protocol optimized for constrained IoT devices over unreliable networks
  • Broker: Central server routing messages from publishers to all matching subscribers by topic pattern
  • Topic: Hierarchical string (e.g., home/bedroom/temperature) used to route messages to interested subscribers
  • QoS Level: Quality of Service 0/1/2 trading delivery guarantee for message overhead
  • Retained Message: Last message on a topic stored by broker for immediate delivery to new subscribers
  • Last Will and Testament: Pre-configured message published by broker when a client disconnects ungracefully
  • Persistent Session: Broker stores subscriptions and pending messages allowing clients to resume after disconnection

28.2 For Beginners: MQTT Exercises

These exercises let you practice MQTT concepts through guided activities. From setting up your first broker connection to building a complete sensor monitoring system, each exercise builds your confidence and skills with the protocol that powers millions of IoT devices worldwide.

28.3 Lab 1: ESP32 DHT22 MQTT Publisher with QoS Levels

Objective: Build a temperature/humidity sensor that publishes to MQTT with different QoS levels.

Materials:

  • ESP32 development board
  • DHT22 temperature/humidity sensor
  • 10k ohm pull-up resistor
  • Breadboard and jumper wires
  • Wi-Fi connection

Circuit Diagram:

DHT22          ESP32
-----          -----
VCC   ------>  3.3V
DATA  ------>  GPIO 4 (with 10k ohm pull-up to 3.3V)
GND   ------>  GND

Complete Code:

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

// Wi-Fi credentials
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// MQTT Broker settings
const char* mqtt_server = "test.mosquitto.org";
const int mqtt_port = 1883;
const char* mqtt_client_id = "ESP32_DHT22_001";

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

// MQTT topics
const char* topic_temp = "iotclass/lab1/temperature";
const char* topic_humidity = "iotclass/lab1/humidity";
const char* topic_status = "iotclass/lab1/status";

WiFiClient espClient;
PubSubClient client(espClient);

unsigned long lastPublish = 0;
const long publishInterval = 5000; // 5 seconds

void setup_wifi() {
  Serial.println("\nConnecting 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());
}

void reconnect_mqtt() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT broker...");

    if (client.connect(mqtt_client_id)) {
      Serial.println(" Connected");

      // Publish online status as retained message
      client.publish(topic_status, "online", true);

      // Subscribe to commands (optional)
      client.subscribe("iotclass/lab1/command");
    } else {
      Serial.print(" Failed, rc=");
      Serial.println(client.state());
      delay(5000);
    }
  }
}

void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message received on ");
  Serial.print(topic);
  Serial.print(": ");

  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);
}

void setup() {
  Serial.begin(115200);
  dht.begin();

  setup_wifi();

  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(mqtt_callback);
}

void loop() {
  if (!client.connected()) {
    reconnect_mqtt();
  }
  client.loop();

  unsigned long now = millis();
  if (now - lastPublish >= publishInterval) {
    lastPublish = now;

    // Read sensor
    float humidity = dht.readHumidity();
    float temperature = dht.readTemperature();

    if (isnan(humidity) || isnan(temperature)) {
      Serial.println("Failed to read from DHT sensor!");
      return;
    }

    // Publish temperature with QoS 0
    char tempStr[8];
    dtostrf(temperature, 6, 2, tempStr);
    bool temp_success = client.publish(topic_temp, tempStr, false);
    Serial.print("Temperature: ");
    Serial.print(tempStr);
    Serial.print("C ");
    Serial.println(temp_success ? "OK" : "FAIL");

    // Publish humidity
    char humStr[8];
    dtostrf(humidity, 6, 2, humStr);
    bool hum_success = client.publish(topic_humidity, humStr, false);
    Serial.print("Humidity: ");
    Serial.print(humStr);
    Serial.print("% ");
    Serial.println(hum_success ? "OK" : "FAIL");

    Serial.println("---");
  }
}

Expected Output (Serial Monitor):

Connecting to Wi-Fi...
Wi-Fi connected
IP address: 192.168.1.100
Connecting to MQTT broker... Connected
Temperature: 22.50C OK
Humidity: 45.30% OK
---
Temperature: 22.48C OK
Humidity: 45.35% OK
---
Interactive Simulator: MQTT Publisher (ESP32 + DHT22)

Try it yourself! See a complete IoT system publishing sensor data to an MQTT broker.

Learning Points:

  • PubSubClient Library: Arduino MQTT client for ESP32
  • client.connect(): Establishes connection to broker with unique client ID
  • client.publish(): Sends message to topic (returns true/false)
  • Topic Hierarchy: iotclass/lab1/temperature uses / separators
  • QoS 0 (At Most Once): Fire-and-forget, fastest but no guarantee

Challenges:

  1. Modify to publish only when temperature changes by +/-0.5C (reduce traffic)
  2. Add battery voltage monitoring and publish with QoS 2
  3. Implement Last Will and Testament to detect unexpected disconnections
  4. Add JSON payload with multiple sensor readings

You’re building a system with 4 sensor types. Which QoS should each use?

Sensor Type Data Frequency Criticality Duplicate Impact Recommended QoS Rationale
Temperature Every 30s Low None (idempotent) QoS 0 Replaceable; next reading comes soon. Fire-and-forget minimizes battery drain
Door open/close On event (~20/day) Medium Low (duplicate alerts acceptable) QoS 1 Important for security logs. Retries ensure delivery. Duplicate “door opened” alerts are annoying but harmless
Smoke alarm trigger On event (~0.1/year) Critical Low (duplicate alarms better than missed) QoS 1 Life-safety critical. Must arrive. Duplicate alarms are preferable to false negatives
Unlock door command On demand (~50/day) Critical High (duplicate = relock) QoS 2 Toggle command: ON-ON-OFF = locked again! Non-idempotent action requires exactly-once guarantee

Key Decision Factors:

  1. Data replaceability: If the next reading supersedes the current one → QoS 0
  2. Event vs telemetry: One-time events need delivery guarantees → QoS 1
  3. Idempotency: If receiving twice is safe → QoS 1. If receiving twice causes problems → QoS 2
  4. Battery constraints: QoS 0 uses ~50% less power than QoS 1, and ~66% less than QoS 2

Mixed QoS Strategy:

# Single MQTT client can publish different topics with different QoS
client.publish("sensors/temp", temp, qos=0)      # Telemetry
client.publish("events/door", "opened", qos=1)   # Alert
client.publish("actuators/lock", "unlock", qos=2) # Command

Cost Impact Example (AWS IoT Core, 10,000 devices): - All QoS 0: 432M msgs/month = $432 - All QoS 1: 864M msgs/month = $864 (2x due to PUBACK) - All QoS 2: 1.73B msgs/month = $1,730 (4x due to handshake) - Mixed strategy (95% QoS 0, 4% QoS 1, 1% QoS 2): $490/month → 72% savings vs all-QoS-2

QoS level directly impacts battery life for devices transmitting the same data. Calculate the energy cost per message.

\[ E_{\text{QoS}} = E_{\text{TX}} \times (1 + N_{\text{handshake}}) + E_{\text{RX}} \times N_{\text{ACK}} \]

Worked example: ESP32 with Wi-Fi radio transmits 100-byte MQTT messages. Radio consumes 240 mA at 3.3V for TX (0.79 W), 100 mA for RX (0.33 W). Each transmission takes 15 ms.

QoS 0 (fire-and-forget): Energy = 0.79 W × 15 ms = 11.85 mJ per message.

QoS 1 (with PUBACK): Energy = (0.79 W × 15 ms TX) + (0.33 W × 12 ms RX for PUBACK) = 11.85 + 3.96 = 15.81 mJ (33% higher).

QoS 2 (4-part handshake): Energy ≈ (0.79 W × 30 ms TX) + (0.33 W × 24 ms RX) = 23.7 + 7.92 = 31.62 mJ (167% higher than QoS 0).

For battery-powered sensor sending 1 msg/minute (60 msgs/hour): QoS 0 uses 711 mJ/hour, QoS 2 uses 1,897 mJ/hour. Over 5-year deployment (43,800 hours), QoS 2 consumes 83.1 kJ vs 31.1 kJ for QoS 0 — requiring 2.7x larger battery.


28.4 Interactive Calculators

28.4.1 QoS Energy Cost Calculator

Estimate the energy cost per MQTT message and projected battery life based on your radio parameters and QoS level.

28.4.2 MQTT Bandwidth Calculator

Calculate the network bandwidth consumed by your MQTT sensor fleet based on device count, payload size, and publish interval.

28.4.3 AWS IoT Core MQTT Cost Estimator

Estimate monthly AWS IoT Core costs based on your MQTT fleet size and QoS usage mix.

28.4.4 MQTT Publish Interval Optimizer

Find the optimal publish interval that balances data freshness against bandwidth and battery constraints.


28.5 Lab 2: Python MQTT Dashboard with Multiple Sensors

Objective: Create a real-time dashboard that subscribes to multiple sensor topics and displays data.

Materials:

  • Python 3.7+
  • paho-mqtt library
  • (Optional) Running ESP32 from Lab 1

Complete Code:

import paho.mqtt.client as mqtt
from datetime import datetime
import json

# MQTT Settings
BROKER = "test.mosquitto.org"
PORT = 1883
TOPICS = [
    ("iotclass/lab1/temperature", 0),
    ("iotclass/lab1/humidity", 0),
    ("iotclass/lab1/status", 0)
]

# Data storage
sensor_data = {}
message_count = 0

def on_connect(client, userdata, flags, reason_code, properties):
    print("=" * 60)
    print("    IoT MQTT Dashboard - Real-Time Sensor Data")
    print("=" * 60)
    print(f"\nConnected with reason code {reason_code}")

    # Subscribe to all topics
    client.subscribe(TOPICS)
    print(f"Subscribed to {len(TOPICS)} topics")
    print("-" * 60)

def on_message(client, userdata, msg):
    global message_count
    message_count += 1

    topic = msg.topic
    payload = msg.payload.decode()
    timestamp = datetime.now().strftime("%H:%M:%S")

    # Extract sensor name from topic
    parts = topic.split("/")
    sensor_name = parts[-1].upper()

    # Store data
    sensor_data[sensor_name] = {
        "value": payload,
        "time": timestamp,
        "qos": msg.qos
    }

    # Display update
    print(f"\n[{timestamp}] Update from {sensor_name}:")
    print(f"  Value: {payload}")

    # Check for alerts
    if sensor_name == "TEMPERATURE":
        try:
            temp = float(payload)
            if temp > 30:
                print("  ⚠ WARNING: High temperature!")
            elif temp < 10:
                print("  ⚠ WARNING: Low temperature!")
        except ValueError:
            pass

    # Show dashboard summary
    print(f"\n--- Dashboard Summary ({message_count} total messages) ---")
    for name, data in sensor_data.items():
        print(f"  {name}: {data['value']} (updated: {data['time']})")

def main():
    # Create client (paho-mqtt 2.0+)
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="Dashboard_001")
    client.on_connect = on_connect
    client.on_message = on_message

    # Connect
    print(f"Connecting to {BROKER}:{PORT}...")
    client.connect(BROKER, PORT)

    # Run forever
    try:
        client.loop_forever()
    except KeyboardInterrupt:
        print("\nDisconnecting...")
        client.disconnect()

if __name__ == "__main__":
    main()

Expected Output:

Connecting to test.mosquitto.org:1883...
============================================================
    IoT MQTT Dashboard - Real-Time Sensor Data
============================================================

Connected with reason code Success
Subscribed to 3 topics
------------------------------------------------------------

[14:32:15] Update from TEMPERATURE:
  Value: 22.50

--- Dashboard Summary (1 total messages) ---
  TEMPERATURE: 22.50 (updated: 14:32:15)

[14:32:15] Update from HUMIDITY:
  Value: 45.30

--- Dashboard Summary (2 total messages) ---
  TEMPERATURE: 22.50 (updated: 14:32:15)
  HUMIDITY: 45.30 (updated: 14:32:15)

28.6 Lab 3: MQTT Home Automation - Lights and Motion

Objective: Build a complete home automation system with motion detection and automated lighting control.

Materials:

  • 2x ESP32 boards (one for motion sensor, one for light control)
  • PIR motion sensor (HC-SR501)
  • LED (or relay module for real lights)
  • 220 ohm resistor
  • Breadboard and wires

Architecture:

PIR motion sensor system
Figure 28.1: Home automation architecture with motion sensor and light control

Code for Motion Sensor (ESP32 #1):

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

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "test.mosquitto.org";

WiFiClient espClient;
PubSubClient client(espClient);

#define PIR_PIN 13
#define ROOM_ID "living_room"

char motion_topic[50];
char light_command_topic[50];

bool last_motion_state = false;
unsigned long motion_start_time = 0;
const unsigned long AUTO_OFF_DELAY = 30000; // 30 seconds

void setup() {
  Serial.begin(115200);
  pinMode(PIR_PIN, INPUT);

  sprintf(motion_topic, "home/%s/motion", ROOM_ID);
  sprintf(light_command_topic, "home/%s/light/command", ROOM_ID);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi connected");

  client.setServer(mqtt_server, 1883);
  reconnect();
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect("ESP32_MotionSensor")) {
      Serial.println(" Connected");
    } else {
      delay(5000);
    }
  }
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  bool motion_detected = digitalRead(PIR_PIN) == HIGH;

  // Motion started
  if (motion_detected && !last_motion_state) {
    Serial.println("Motion detected!");
    client.publish(motion_topic, "true", true);

    // Turn on light
    client.publish(light_command_topic, "ON");
    motion_start_time = millis();
    last_motion_state = true;
  }

  // Motion stopped
  if (!motion_detected && last_motion_state) {
    Serial.println("Motion cleared");
    client.publish(motion_topic, "false", true);
    last_motion_state = false;
  }

  // Auto turn off light after delay
  if (!motion_detected && (millis() - motion_start_time > AUTO_OFF_DELAY)) {
    client.publish(light_command_topic, "OFF");
    Serial.println("Auto turning off light");
    motion_start_time = millis() + 1000000; // Prevent repeated commands
  }

  delay(200);
}

Code for Light Control (ESP32 #2):

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

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "test.mosquitto.org";

WiFiClient espClient;
PubSubClient client(espClient);

#define LED_PIN 2
#define ROOM_ID "living_room"

char light_command_topic[50];
char light_state_topic[50];

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

  Serial.print("Received command: ");
  Serial.println(message);

  if (message == "ON") {
    digitalWrite(LED_PIN, HIGH);
    client.publish(light_state_topic, "ON", true);
    Serial.println("Light turned ON");
  } else if (message == "OFF") {
    digitalWrite(LED_PIN, LOW);
    client.publish(light_state_topic, "OFF", true);
    Serial.println("Light turned OFF");
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  sprintf(light_command_topic, "home/%s/light/command", ROOM_ID);
  sprintf(light_state_topic, "home/%s/light/state", ROOM_ID);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi connected");

  client.setServer(mqtt_server, 1883);
  client.setCallback(mqtt_callback);

  reconnect();
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect("ESP32_LightControl")) {
      Serial.println(" Connected");

      // Subscribe to light commands
      client.subscribe(light_command_topic);
      Serial.print("Subscribed to: ");
      Serial.println(light_command_topic);

      // Publish initial state
      client.publish(light_state_topic, "OFF", true);
    } else {
      delay(5000);
    }
  }
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}
Interactive Simulator: MQTT Subscriber (Light Control)

What This Simulates: ESP32 subscribing to MQTT commands and controlling an LED based on received messages.

Learning Points:

  • mqtt_callback() executes when messages arrive
  • client.subscribe() registers interest in topics
  • LED state changes based on “ON”/“OFF” payload
  • Retained messages store current state for new subscribers

28.7 MQTT Broker Interactive Game

Master MQTT concepts through an interactive game where you act as an MQTT broker, routing messages from publishers to the correct subscribers.


28.8 Practice Exercises

28.8.1 Exercise 1: Multi-Room Temperature Monitor with CSV Logging

Objective: Extend Lab 1 by adding persistent data logging and multi-room alert thresholds.

Tasks:

  1. Configure two ESP32 publishers for different rooms (living room and bedroom) using distinct topic hierarchies
  2. Create a Python subscriber that logs all readings to a timestamped CSV file
  3. Implement per-room alert thresholds (e.g., bedroom max 25C, living room max 30C)
  4. Add a wildcard subscription (home/+/temperature) to receive from all rooms with a single subscriber

28.8.2 Exercise 2: Secure MQTT with TLS/SSL

Objective: Implement production-grade MQTT security with encryption.

Tasks:

  1. Generate self-signed certificate for local Mosquitto broker
  2. Configure Mosquitto for TLS on port 8883
  3. Create Python MQTT client with TLS
  4. Test that unencrypted connections are rejected

28.8.3 Exercise 3: MQTT-to-Database Pipeline

Objective: Build a complete data ingestion pipeline from MQTT to persistent storage.

Tasks:

  1. Set up SQLite database for time-series sensor data
  2. Create MQTT subscriber that writes to database
  3. Simulate 5 different sensors publishing various metrics
  4. Query database for average temperature over last hour

28.9 Knowledge Check

28.10 See Also

Lab structure: Each lab builds on the previous, progressing from simple publisher to dashboard subscriber to multi-device automation to secure deployment.

Common Pitfalls

Unencrypted MQTT exposes device credentials and sensor data to network eavesdroppers — in a building IoT deployment on shared WiFi, this means any connected device can read all sensor data. Always enable TLS 1.2+ on the broker and generate unique client certificates for each device class.

Without LWT, there is no automatic notification when a device disconnects ungracefully — missed timeout alarms and false-healthy device status are common consequences. Configure LWT on every device connection to publish an offline status message, enabling real-time fleet health monitoring.

A single MQTT connection serializes all publishes through one TCP socket — at 100 messages/second with QoS 1, TCP backpressure creates queuing latency. Use multiple parallel MQTT connections or partition topics across connection pools for throughput above 1,000 messages/second.

28.11 What’s Next

Chapter Focus Why Read It
MQTT Advanced Topics Broker clustering, high availability, and production deployment Apply the lab skills to production-scale systems with redundancy and monitoring
MQTT Security TLS/SSL encryption, authentication, and access control lists Implement the TLS exercise from Lab 2 and secure broker deployments
MQTT Python Patterns Async subscribers, connection pooling, and error handling Extend the Lab 2 dashboard with robust production-grade Python code
MQTT Introduction Core protocol concepts, packet structure, and topic design Revisit the theory behind the retained messages and QoS mechanics used in Labs 1–3
Sensor Integration DHT22 wiring, calibration, and reading reliability Troubleshoot hardware issues encountered in Lab 1 circuit assembly
ESP32 Prototyping Breadboard setup, GPIO configuration, and power budgeting Resolve hardware setup challenges and plan battery-powered deployments