These hands-on labs cover building complete MQTT systems with real hardware: ESP32 DHT22 temperature publishers with configurable QoS levels, multi-device motion-to-lighting automation pipelines, secure broker deployment with TLS and ACLs, and Last Will Testament (LWT) for automatic device disconnection detection.
32.1 Learning Objectives
By the end of this chapter, you will be able to:
Construct ESP32 MQTT Publishers: Implement complete sensor-to-broker data pipelines with DHT22 temperature sensors using the PubSubClient library
Design Multi-Device Automation Systems: Configure motion sensors and lighting actuators to communicate through an MQTT broker backbone
Select and Justify QoS Levels: Evaluate QoS 0, 1, and 2 trade-offs and select the appropriate level based on reliability requirements and power constraints
Configure Secure MQTT Brokers: Deploy Mosquitto with TLS certificates, username/password authentication, and ACLs for production-ready operation
Implement Last Will Testament: Configure LWT messages to automatically detect and report unexpected device disconnections
Calculate Battery Life Impact: Analyze the energy cost of different QoS levels and compare always-on versus deep-sleep publishing strategies
Key Concepts
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
32.2 For Beginners: MQTT Hands-On Labs
These labs guide you through building real MQTT systems step by step. You will set up a broker, connect devices, publish sensor data, and subscribe to updates. It is like assembling furniture with instructions – each step builds on the previous one until you have a complete, working MQTT application.
32.3 Prerequisites
Before diving into this chapter, you should be familiar with:
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
***
QoS 0 (At Most Once): Fire-and-forget, fastest but no guarantee
Retained Messages: client.publish(topic, msg, true) stores last value for new subscribers
Auto-Reconnect: if (!client.connected()) reconnect_mqtt()
MQTT Publishing Flow:
1. ESP32 connects to Wi-Fi network
2. ESP32 connects to MQTT broker (test.mosquitto.org:1883)
3. ESP32 publishes "online" status as retained message
4. Every 5 seconds:
a. Read DHT22 sensor (temperature, humidity)
b. Convert float to string
c. Publish to "iotclass/lab1/temperature" topic
d. Publish to "iotclass/lab1/humidity" topic
e. Print success/failure indicators
5. Maintain connection with client.loop()
MQTT Topic Structure:
Figure 32.1: MQTT topic hierarchy for lab sensor data
Quality of Service Levels:
QoS 0 (At Most Once): Fast, no acknowledgment - used here
QoS 1 (At Least Once): Acknowledged, may deliver duplicates
QoS 2 (Exactly Once): Slowest, guaranteed single delivery
Real-World Applications:
Smart Agriculture: Soil moisture sensors publishing to cloud dashboard
Industrial Monitoring: Temperature/pressure sensors in manufacturing
Home Automation: Smart thermostats publishing temperature readings
Environmental Monitoring: Weather stations sending data to aggregation service
Worked Example: Optimizing Sensor Publish Frequency
Problem: Your DHT22 ESP32 sensors are publishing every 5 seconds (17,280 messages/day per sensor). With 50 sensors, that’s 864,000 messages/day. AWS IoT Core charges $1/million messages, but you’re also hitting broker CPU limits.
Analysis:
Temperature in a room rarely changes more than 0.5°C per minute. Publishing every 5 seconds means 99% of messages report identical values.
Step 1: Calculate Current Costs
Messages/month: 864,000 × 30 = 25.9M messages
AWS IoT Core: 25.9 × $1 = $25.90/month
Broker CPU: Mosquitto on m5.large = $70/month
Total: $95.90/month
Step 2: Design Optimization
Publish only when temperature changes by ≥0.5°C OR 5 minutes elapse (heartbeat):
float lastTemp =0.0;unsignedlong lastPublish =0;constfloat THRESHOLD =0.5;// 0.5°C change triggers publishconstunsignedlong HEARTBEAT =300000;// 5 min max intervalvoid loop(){float temp = dht.readTemperature();unsignedlong now = millis();// Publish if temp changed OR heartbeat timeoutif(abs(temp - lastTemp)>= THRESHOLD ||(now - lastPublish >= HEARTBEAT)){ client.publish(topic_temp, String(temp).c_str()); lastTemp = temp; lastPublish = now;} delay(5000);// Still sample every 5s, but don't always publish}
Step 3: Calculate New Costs
Typical office temperature changes 2-3 times/hour (HVAC cycles). With 5-minute heartbeat:
Meaningful changes: ~3/hour = 72/day
Heartbeats: (1440 min/day) / 5 = 288/day
Total: 360 messages/day per sensor (was 17,280)
For 50 sensors: - Messages/month: 360 × 50 × 30 = 540,000 messages - AWS IoT Core: 0.54 × $1 = $0.54/month - Broker CPU: Can downgrade to t3.small = $15/month - New total: $15.54/month → 83% cost reduction
Battery life: Publishing less often extends battery 48x (still sampling, but not transmitting)
Storage costs: Time-series DB costs drop proportionally
Key Insight: Don’t blindly publish on a timer. Publish on change with a heartbeat fallback. This pattern works for most slowly-changing telemetry (temperature, humidity, pressure, battery voltage).
The Mistake: Developers deploy IoT systems without considering broker connection limits. They test with 10-20 devices successfully, then deploy 500+ devices to production. New devices fail to connect with cryptic errors like “connection refused” or timeout, while existing connections work fine.
Why It Happens: MQTT brokers have configurable maximum connection limits, often defaulting to 1,024 (OS file descriptor limit) or lower. Each MQTT connection consumes a file descriptor, memory for session state (~2-10KB), and a TCP socket. When limits are reached, new connections are silently rejected without clear error messages.
The Fix: Calculate connection requirements before deployment. Configure broker limits explicitly. Implement connection health monitoring and alerting when approaching 80% capacity. Use connection pooling or MQTT bridge patterns for high-scale deployments.
Capacity Planning Formula: Required connections = (devices x 1.2) + (backend_services x 2) + (monitoring x 3). For 1,000 devices with 5 backend services and 2 monitoring tools, plan for: (1000 x 1.2) + (5 x 2) + (2 x 3) = 1,216 connections minimum.
32.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
Expected Output:
IoT MQTT Dashboard - Real-Time Sensor Data
LAB1
Temperature 22.50C [14:32:15] QoS:0
Humidity 45.30% [14:32:15] QoS:0
Status Status: online [14:32:10] QoS:0
Total messages received: 127
Last update: 14:32:15
Press Ctrl+C to exit
Learning Outcomes:
Subscribe to multiple MQTT topics with wildcards
Build real-time data visualization
Implement alert systems based on sensor data
Handle MQTT callbacks and data processing
Create user-friendly console interfaces
32.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
Circuit 1 - Motion Sensor (ESP32 #1):
PIR Sensor ESP32
---------- -----
VCC ------> 5V
OUT ------> GPIO 13
GND ------> GND
Circuit 2 - Light Control (ESP32 #2):
ESP32 LED
----- ---
GPIO 2 ----> LED Anode (through 220 ohm resistor)
GND ----> LED Cathode
Wi-Fi connected
Connecting to MQTT... Connected
Motion detected!
Motion cleared
Auto turning off light
Expected Serial Output (Light Control):
Wi-Fi connected
Connecting to MQTT... Connected
Subscribed to: home/living_room/light/command
Received command: ON
Light turned ON
Received command: OFF
Light turned OFF
Implement automation logic with sensors and actuators
Use retained messages for state synchronization
Create topic naming conventions for home automation
Handle timing and auto-off functionality
Challenges:
Add manual control via MQTT (smartphone app or Node-RED)
Implement brightness control with PWM
Add multiple rooms with independent control
Create schedules (morning/evening modes)
Try It: Motion-to-Light Automation Timing
Experiment with motion sensor timing parameters to understand how AUTO_OFF_DELAY, PIR hold time, and sensor polling rate affect automation behavior and power consumption.
Show code
viewof autoOffDelay = Inputs.range([5,300], {value:30,step:5,label:"Auto-off delay (seconds)"})viewof pirHoldTime = Inputs.range([1,30], {value:8,step:1,label:"PIR hold time (seconds)"})viewof pollRate = Inputs.range([50,2000], {value:200,step:50,label:"Sensor poll rate (ms)"})viewof avgMotionEvents = Inputs.range([1,60], {value:10,step:1,label:"Motion events per hour"})
DO:
- Set LWT on critical devices
- Use retain=true for status topics
- Set QoS 1 for LWT
- Include timestamp in LWT message
- Subscribe to +/status for monitoring
DON'T:
- Use large LWT messages (keep <100 bytes)
- Set very short keep-alive (<30s)
- Forget to publish "online" after connect
- Use QoS 0 for LWT (unreliable)
Example LWT message:
{
"status": "offline",
"reason": "unexpected",
"timestamp": 1698765432,
"last_seen": "2024-10-28T10:23:52Z"
}
Try It: MQTT Keep-Alive and LWT Timeout Planner
Configure keep-alive and LWT settings to balance between fast disconnect detection and network overhead. The broker detects a dead client after 1.5x the keep-alive interval (per MQTT spec).
Pitfall: Stale Retained Messages After Device Removal
The Mistake: Developers use retained messages for device status (e.g., devices/sensor42/status with retain=true), but when devices are decommissioned or replaced, the old retained messages remain on the broker indefinitely. New subscribers receive stale “online” status for devices that no longer exist.
Why It Happens: Retained messages persist until explicitly cleared with an empty payload. Most developers focus on the “publish” side and forget that retained messages require lifecycle management. Brokers like Mosquitto will keep retained messages forever unless configured otherwise.
The Fix: Implement device decommissioning that clears retained messages. Publish an empty payload (zero-length) with retain=true to delete the retained message. For MQTT 5.0, use Message Expiry Interval to auto-expire stale status.
# MQTT 3.1.1: Clear retained message on device removaldef decommission_device(client, device_id):# Publish empty payload with retain=true to clear client.publish(f"devices/{device_id}/status", payload="", qos=1, retain=True) client.publish(f"devices/{device_id}/config", payload="", qos=1, retain=True) client.publish(f"devices/{device_id}/lwt", payload="", qos=1, retain=True)print(f"Cleared all retained messages for {device_id}")# MQTT 5.0: Auto-expire status messagesfrom paho.mqtt.properties import Propertiesfrom paho.mqtt.packettypes import PacketTypesprops = Properties(PacketTypes.PUBLISH)props.MessageExpiryInterval =300# Expire after 5 minutes of no updateclient.publish("devices/sensor01/status", payload='{"status": "online", "timestamp": 1699123456}', qos=1, retain=True, properties=props)# If device stops publishing, status auto-clears after 5 minutes
Broker Configuration (Mosquitto): Set retain_available false to disable retained messages entirely if your application doesn’t need them, or use $SYS/broker/retained messages/count to monitor accumulation.
Pitfall: Last Will Triggered on Graceful Disconnect
The Mistake: Developers configure Last Will and Testament (LWT) to publish “offline” status, expecting it only fires on crashes or network failures. But they observe LWT messages being published even during normal shutdown sequences, flooding monitoring systems with false “offline” alerts.
Why It Happens: The MQTT spec states LWT is published when the broker closes the connection “without receiving a DISCONNECT packet.” Many developers call client.disconnect() but don’t wait for completion, or the TCP connection closes before the DISCONNECT packet is sent. Network issues during graceful shutdown can also prevent DISCONNECT delivery.
The Fix: Always explicitly publish “offline” status before disconnecting, then send DISCONNECT. Use blocking disconnect or wait for confirmation. For Python paho-mqtt, use disconnect() followed by loop_stop() in the correct order.
// ESP32: Graceful shutdown with explicit statusvoid gracefulShutdown(){// Step 1: Publish explicit offline status mqttClient.publish("devices/sensor01/status","offline",true);// retain=true delay(100);// Allow time for publish to complete// Step 2: Send DISCONNECT packet (prevents LWT) mqttClient.disconnect(); delay(100);// Allow TCP to close cleanly// Step 3: Now safe to power down WiFi.disconnect(true); esp_deep_sleep_start();}// If LWT fires anyway, it's redundant (already sent "offline")// Better than missing offline status if crash occurs before explicit publish
# Python paho-mqtt 2.0+: Proper disconnect sequenceimport timedef graceful_disconnect(client):# Publish explicit status first result = client.publish("devices/sensor01/status", "offline", qos=1, retain=True) result.wait_for_publish(timeout=5.0) # Block until published# Now disconnect - this prevents LWT from firing client.disconnect() client.loop_stop() # Stop network thread AFTER disconnect# Common mistake: loop_stop() before disconnect()# This kills the network thread before DISCONNECT packet is sent# Result: Broker never receives DISCONNECT, triggers LWT
MQTT 5.0 Enhancement: Use Reason Code in DISCONNECT to tell broker why you’re disconnecting. Reason Code 0x00 (Normal disconnection) explicitly signals “don’t publish LWT.”
Battery Life Calculation:
Scenario: Send temperature every 60 seconds
QoS 0:
- Assumptions: ~120 mA radio current, ~50 ms radio-on time per publish
- Per message: 120 mA x 50 ms / 3,600,000 = 0.0017 mAh
- Per day: 1440 x 0.0017 = 2.4 mAh/day
- Battery (2000 mAh): 2000 / 2.4 = ~833 days (~2.3 years)
QoS 1:
- Assumptions: ~120 mA radio current, ~150 ms radio-on time (PUBLISH + PUBACK)
- Per message: 120 mA x 150 ms / 3,600,000 = 0.0050 mAh
- Per day: 1440 x 0.0050 = 7.2 mAh/day
- Battery (2000 mAh): 2000 / 7.2 = ~278 days (~9 months)
QoS 2:
- Assumptions: ~120 mA radio current, ~300 ms radio-on time (4-way handshake)
- Per message: 120 mA x 300 ms / 3,600,000 = 0.0100 mAh
- Per day: 1440 x 0.0100 = 14.4 mAh/day
- Battery (2000 mAh): 2000 / 14.4 = ~139 days (~4.6 months)
Conclusion: QoS 0 uses ~67% less TX energy than QoS 1; QoS 2 has the highest overhead.
Note: These estimates isolate MQTT exchange time. If the device reconnects Wi-Fi/TLS each minute (common with deep sleep), connection overhead will dominate and battery life will be much lower.
Putting Numbers to It
The QoS battery calculations above assume “always-on” Wi-Fi. With ESP32 deep sleep, connection overhead dominates:
Deep sleep scenario (ESP32 wakes every 60 s, sends 1 reading, sleeps):
Wi-Fi connection overhead:
Scan channels: 200 ms @ 80 mA = 4.44 µAh
Associate + DHCP: 1,500 ms @ 100 mA = 41.67 µAh
Total connection: 46.11 µAh per wake
MQTT connection (clean session): - TCP handshake: 150 ms @ 80 mA = 3.33 µAh - CONNECT + CONNACK: 100 ms @ 80 mA = 2.22 µAh - Total MQTT setup: 5.55 µAh
Battery life (2000 mAh): \[\frac{2{,}000}{76.8} = 26 \text{ days}\]
Key insight: Wi-Fi connection (46 µAh) is 28× larger than the MQTT message (1.67 µAh). Persistent session with keepalive = 5 min eliminates reconnection overhead → 833 days battery life!
32.9 Interactive Design Tools
Use these calculators to plan your MQTT deployments with real numbers.
32.9.1 MQTT QoS Battery Life Calculator
Estimate how long your battery-powered MQTT device will last at each QoS level.