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.
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
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.
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( " \n Connecting to Wi-Fi..." );
WiFi. begin( ssid, password);
while ( WiFi. status() != WL_CONNECTED) {
delay( 500 );
Serial. print( "." );
}
Serial. println( " \n Wi-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
---
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:
Modify to publish only when temperature changes by +/-0.5C (reduce traffic)
Add battery voltage monitoring and publish with QoS 2
Implement Last Will and Testament to detect unexpected disconnections
Add JSON payload with multiple sensor readings
You’re building a system with 4 sensor types. Which QoS should each use?
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 :
Data replaceability : If the next reading supersedes the current one → QoS 0
Event vs telemetry : One-time events need delivery guarantees → QoS 1
Idempotency : If receiving twice is safe → QoS 1. If receiving twice causes problems → QoS 2
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.
Interactive Calculators
QoS Energy Cost Calculator
Estimate the energy cost per MQTT message and projected battery life based on your radio parameters and QoS level.
Show code
viewof txPower = Inputs. range ([0.1 , 2.0 ], {
value : 0.79 ,
step : 0.01 ,
label : "TX power (W)"
})
viewof rxPower = Inputs. range ([0.05 , 1.0 ], {
value : 0.33 ,
step : 0.01 ,
label : "RX power (W)"
})
viewof txTime = Inputs. range ([1 , 100 ], {
value : 15 ,
step : 1 ,
label : "TX time per packet (ms)"
})
viewof qosLevel = Inputs. radio (["QoS 0" , "QoS 1" , "QoS 2" ], {
value : "QoS 0" ,
label : "QoS Level"
})
viewof msgsPerMin = Inputs. range ([0.1 , 60 ], {
value : 1 ,
step : 0.1 ,
label : "Messages per minute"
})
viewof batteryCapacity = Inputs. range ([100 , 20000 ], {
value : 3000 ,
step : 100 ,
label : "Battery capacity (mAh)"
})
viewof batteryVoltage = Inputs. range ([1.8 , 5.0 ], {
value : 3.3 ,
step : 0.1 ,
label : "Battery voltage (V)"
})
qosMultiplier = qosLevel === "QoS 0" ? {txMult : 1 , rxMult : 0 , label : "fire-and-forget" } :
qosLevel === "QoS 1" ? {txMult : 1 , rxMult : 0.8 , label : "at-least-once (PUBACK)" } :
{txMult : 2 , rxMult : 1.6 , label : "exactly-once (4-step handshake)" }
energyPerMsg = (txPower * txTime * qosMultiplier. txMult / 1000 ) + (rxPower * txTime * qosMultiplier. rxMult / 1000 )
energyPerMsgMJ = energyPerMsg * 1000
energyPerHour = energyPerMsgMJ * msgsPerMin * 60
batteryEnergyJ = batteryCapacity / 1000 * batteryVoltage * 3600
batteryLifeHours = batteryEnergyJ / (energyPerHour / 1000 )
batteryLifeDays = batteryLifeHours / 24
batteryLifeYears = (batteryLifeDays / 365 ). toFixed (2 )
html `<div style="background: #f8f9fa; padding: 20px; border-left: 4px solid #16A085; margin: 20px 0; border-radius: 4px;">
<h4 style="color: #2C3E50; margin-top: 0;">Energy Cost Results ( ${ qosLevel} - ${ qosMultiplier. label } )</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin: 15px 0;">
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Energy per Message</div>
<div style="color: #16A085; font-size: 1.8em; font-weight: bold;"> ${ energyPerMsgMJ. toFixed (2 )} mJ</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Energy per Hour</div>
<div style="color: #E67E22; font-size: 1.8em; font-weight: bold;"> ${ energyPerHour. toFixed (1 )} mJ</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Estimated Battery Life</div>
<div style="color: #3498DB; font-size: 1.8em; font-weight: bold;"> ${ batteryLifeYears} yrs</div>
<div style="color: #7F8C8D; font-size: 0.8em;">( ${ Math . round (batteryLifeDays)} days)</div>
</div>
</div>
<div style="background: linear-gradient(135deg, #2C3E50 0%, #16A085 100%); color: white; padding: 12px; border-radius: 4px; text-align: center; margin-top: 10px;">
<span style="font-size: 0.9em;">At ${ msgsPerMin} msg/min with a ${ batteryCapacity} mAh battery, MQTT radio active time is
<strong> ${ (msgsPerMin * 60 * txTime * (qosMultiplier. txMult + qosMultiplier. rxMult ) / 1000 / 3600 * 100 ). toFixed (3 )} %</strong> of total time</span>
</div>
</div>`
MQTT Bandwidth Calculator
Calculate the network bandwidth consumed by your MQTT sensor fleet based on device count, payload size, and publish interval.
Show code
viewof deviceCount = Inputs. range ([1 , 10000 ], {
value : 100 ,
step : 1 ,
label : "Number of devices"
})
viewof publishInterval = Inputs. range ([1 , 3600 ], {
value : 30 ,
step : 1 ,
label : "Publish interval (seconds)"
})
viewof payloadSize = Inputs. range ([10 , 2000 ], {
value : 50 ,
step : 10 ,
label : "Payload size (bytes)"
})
viewof topicLength = Inputs. range ([5 , 100 ], {
value : 25 ,
step : 1 ,
label : "Avg topic length (chars)"
})
viewof bwQosLevel = Inputs. radio (["QoS 0" , "QoS 1" , "QoS 2" ], {
value : "QoS 0" ,
label : "QoS Level"
})
mqttOverhead = 2 + topicLength + 2
qosPacketMult = bwQosLevel === "QoS 0" ? 1 : bwQosLevel === "QoS 1" ? 2 : 4
totalMsgSize = mqttOverhead + payloadSize
msgsPerSecTotal = deviceCount / publishInterval
bytesPerSec = msgsPerSecTotal * totalMsgSize * qosPacketMult
kbPerSec = bytesPerSec / 1024
mbPerHour = bytesPerSec * 3600 / (1024 * 1024 )
gbPerMonth = mbPerHour * 24 * 30 / 1024
html `<div style="background: #f8f9fa; padding: 20px; border-left: 4px solid #3498DB; margin: 20px 0; border-radius: 4px;">
<h4 style="color: #2C3E50; margin-top: 0;">Bandwidth Estimation ( ${ bwQosLevel} )</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 15px 0;">
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Messages per Second (fleet)</div>
<div style="color: #2C3E50; font-size: 1.8em; font-weight: bold;"> ${ msgsPerSecTotal. toFixed (1 )} </div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Per-Message Size (with MQTT header)</div>
<div style="color: #16A085; font-size: 1.8em; font-weight: bold;"> ${ totalMsgSize} B</div>
<div style="color: #7F8C8D; font-size: 0.8em;"> ${ mqttOverhead} B overhead + ${ payloadSize} B payload</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Throughput</div>
<div style="color: #E67E22; font-size: 1.8em; font-weight: bold;"> ${ kbPerSec. toFixed (2 )} KB/s</div>
<div style="color: #7F8C8D; font-size: 0.8em;"> ${ mbPerHour. toFixed (1 )} MB/hour</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Monthly Data Transfer</div>
<div style="color: #3498DB; font-size: 1.8em; font-weight: bold;"> ${ gbPerMonth. toFixed (2 )} GB</div>
<div style="color: #7F8C8D; font-size: 0.8em;"> ${ qosPacketMult} x packet multiplier for ${ bwQosLevel} </div>
</div>
</div>
<div style="margin-top: 10px; padding: 10px; background: #e8f4f8; border-radius: 4px; font-size: 0.85em; color: #2C3E50;">
<strong>Tip:</strong> MQTT's small fixed header (2 bytes minimum) makes it ${ ((totalMsgSize + 200 ) / totalMsgSize). toFixed (1 )} x more efficient than HTTP for the same payload, saving approximately ${ ((200 * msgsPerSecTotal * 3600 * 24 * 30 ) / (1024 * 1024 * 1024 )). toFixed (2 )} GB/month of HTTP overhead.
</div>
</div>`
AWS IoT Core MQTT Cost Estimator
Estimate monthly AWS IoT Core costs based on your MQTT fleet size and QoS usage mix.
Show code
viewof awsDevices = Inputs. range ([10 , 100000 ], {
value : 10000 ,
step : 10 ,
label : "Number of devices"
})
viewof awsMsgsPerDevice = Inputs. range ([1 , 1440 ], {
value : 120 ,
step : 1 ,
label : "Messages per device per hour"
})
viewof awsQos0Pct = Inputs. range ([0 , 100 ], {
value : 95 ,
step : 1 ,
label : "QoS 0 traffic (%)"
})
viewof awsQos1Pct = Inputs. range ([0 , 100 ], {
value : 4 ,
step : 1 ,
label : "QoS 1 traffic (%)"
})
awsQos2Pct = Math . max (0 , 100 - awsQos0Pct - awsQos1Pct)
awsTotalMsgsPerHour = awsDevices * awsMsgsPerDevice
awsTotalMsgsPerMonth = awsTotalMsgsPerHour * 24 * 30
awsQos0Msgs = awsTotalMsgsPerMonth * awsQos0Pct / 100
awsQos1Msgs = awsTotalMsgsPerMonth * awsQos1Pct / 100 * 2
awsQos2Msgs = awsTotalMsgsPerMonth * awsQos2Pct / 100 * 4
awsBillableTotal = awsQos0Msgs + awsQos1Msgs + awsQos2Msgs
awsPricePerMillion = 1.00
awsMonthlyCost = (awsBillableTotal / 1000000 ) * awsPricePerMillion
awsAllQos0Cost = (awsTotalMsgsPerMonth / 1000000 ) * awsPricePerMillion
awsAllQos2Cost = (awsTotalMsgsPerMonth * 4 / 1000000 ) * awsPricePerMillion
awsSavings = awsAllQos2Cost > 0 ? ((awsAllQos2Cost - awsMonthlyCost) / awsAllQos2Cost * 100 ). toFixed (1 ) : 0
html `<div style="background: #f8f9fa; padding: 20px; border-left: 4px solid #E67E22; margin: 20px 0; border-radius: 4px;">
<h4 style="color: #2C3E50; margin-top: 0;">AWS IoT Core Cost Estimate</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin: 15px 0;">
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Base Messages / Month</div>
<div style="color: #2C3E50; font-size: 1.5em; font-weight: bold;"> ${ (awsTotalMsgsPerMonth / 1e6 ). toFixed (1 )} M</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Billable Messages (with QoS overhead)</div>
<div style="color: #E67E22; font-size: 1.5em; font-weight: bold;"> ${ (awsBillableTotal / 1e6 ). toFixed (1 )} M</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">QoS 2 Allocation</div>
<div style="color: #9B59B6; font-size: 1.5em; font-weight: bold;"> ${ awsQos2Pct} %</div>
<div style="color: #7F8C8D; font-size: 0.8em;">(auto-calculated remainder)</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin: 15px 0;">
<div style="background: white; padding: 15px; border-radius: 4px; border-left: 4px solid #16A085;">
<div style="color: #7F8C8D; font-size: 0.85em;">Your Mixed Strategy</div>
<div style="color: #16A085; font-size: 2em; font-weight: bold;">$ ${ awsMonthlyCost. toFixed (0 )} </div>
<div style="color: #7F8C8D; font-size: 0.8em;">per month</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border-left: 4px solid #7F8C8D;">
<div style="color: #7F8C8D; font-size: 0.85em;">If All QoS 0</div>
<div style="color: #7F8C8D; font-size: 2em; font-weight: bold;">$ ${ awsAllQos0Cost. toFixed (0 )} </div>
<div style="color: #7F8C8D; font-size: 0.8em;">per month (minimum)</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border-left: 4px solid #E74C3C;">
<div style="color: #7F8C8D; font-size: 0.85em;">If All QoS 2</div>
<div style="color: #E74C3C; font-size: 2em; font-weight: bold;">$ ${ awsAllQos2Cost. toFixed (0 )} </div>
<div style="color: #7F8C8D; font-size: 0.8em;">per month (maximum)</div>
</div>
</div>
<div style="background: linear-gradient(135deg, #E67E22 0%, #E74C3C 100%); color: white; padding: 12px; border-radius: 4px; text-align: center;">
<span style="font-size: 0.9em;">Mixed QoS strategy saves <strong> ${ awsSavings} %</strong> compared to all-QoS-2 ($ ${ (awsAllQos2Cost - awsMonthlyCost). toFixed (0 )} /month)</span>
</div>
</div>`
MQTT Publish Interval Optimizer
Find the optimal publish interval that balances data freshness against bandwidth and battery constraints.
Show code
viewof sensorChangeRate = Inputs. range ([0.01 , 5.0 ], {
value : 0.5 ,
step : 0.01 ,
label : "Sensor change rate (units/min)"
})
viewof acceptableError = Inputs. range ([0.1 , 10.0 ], {
value : 1.0 ,
step : 0.1 ,
label : "Acceptable staleness error (units)"
})
viewof optPayloadBytes = Inputs. range ([10 , 500 ], {
value : 50 ,
step : 5 ,
label : "Message payload (bytes)"
})
viewof optDeviceCount = Inputs. range ([1 , 1000 ], {
value : 50 ,
step : 1 ,
label : "Number of sensors"
})
viewof dailyBudgetMB = Inputs. range ([1 , 1000 ], {
value : 100 ,
step : 1 ,
label : "Daily bandwidth budget (MB)"
})
optimalInterval = acceptableError / sensorChangeRate * 60
maxMsgsPerDay = (dailyBudgetMB * 1024 * 1024 ) / (optPayloadBytes + 29 ) / optDeviceCount
budgetInterval = 86400 / maxMsgsPerDay
recommendedInterval = Math . max (optimalInterval, budgetInterval)
msgsPerDayAtRecommended = 86400 / recommendedInterval * optDeviceCount
dailyDataMB = msgsPerDayAtRecommended * (optPayloadBytes + 29 ) / (1024 * 1024 )
avgStaleness = (recommendedInterval / 60 * sensorChangeRate / 2 ). toFixed (2 )
isConstrained = budgetInterval > optimalInterval
html `<div style="background: #f8f9fa; padding: 20px; border-left: 4px solid #9B59B6; margin: 20px 0; border-radius: 4px;">
<h4 style="color: #2C3E50; margin-top: 0;">Publish Interval Recommendation</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin: 15px 0;">
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Freshness-Optimal Interval</div>
<div style="color: #16A085; font-size: 1.8em; font-weight: bold;"> ${ optimalInterval. toFixed (1 )} s</div>
<div style="color: #7F8C8D; font-size: 0.8em;">keeps error below ${ acceptableError} units</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Budget-Constrained Interval</div>
<div style="color: #E67E22; font-size: 1.8em; font-weight: bold;"> ${ budgetInterval. toFixed (1 )} s</div>
<div style="color: #7F8C8D; font-size: 0.8em;">fits within ${ dailyBudgetMB} MB/day</div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border-left: 4px solid ${ isConstrained ? '#E74C3C' : '#16A085' } ;">
<div style="color: #7F8C8D; font-size: 0.85em;">Recommended Interval</div>
<div style="color: ${ isConstrained ? '#E74C3C' : '#16A085' } ; font-size: 1.8em; font-weight: bold;"> ${ recommendedInterval. toFixed (1 )} s</div>
<div style="color: #7F8C8D; font-size: 0.8em;"> ${ isConstrained ? 'bandwidth-limited' : 'freshness-optimal' } </div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 15px 0;">
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Fleet Messages per Day</div>
<div style="color: #3498DB; font-size: 1.5em; font-weight: bold;"> ${ Math . round (msgsPerDayAtRecommended). toLocaleString ()} </div>
</div>
<div style="background: white; padding: 15px; border-radius: 4px; border: 1px solid #ddd;">
<div style="color: #7F8C8D; font-size: 0.85em;">Daily Data Usage</div>
<div style="color: #9B59B6; font-size: 1.5em; font-weight: bold;"> ${ dailyDataMB. toFixed (1 )} MB</div>
<div style="color: #7F8C8D; font-size: 0.8em;"> ${ (dailyDataMB / dailyBudgetMB * 100 ). toFixed (0 )} % of budget</div>
</div>
</div>
<div style="margin-top: 10px; padding: 10px; background: ${ isConstrained ? '#fce4e4' : '#e8f8f0' } ; border-radius: 4px; font-size: 0.85em; color: #2C3E50;">
<strong> ${ isConstrained ? 'Warning:' : 'Status:' } </strong> ${ isConstrained ? 'Bandwidth budget forces a longer interval than freshness requires. Average data staleness: ' + avgStaleness + ' units (max acceptable: ' + acceptableError + '). Consider increasing the daily budget or reducing device count.' : 'Bandwidth budget is sufficient for freshness-optimal publishing. Average data staleness: ' + avgStaleness + ' units, well within the ' + acceptableError + ' unit tolerance.' }
</div>
</div>`
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" \n Connected 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 (" \n Disconnecting..." )
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)
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:
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( " \n Wi-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( " \n Wi-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();
}
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
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.
Show code
viewof gameLevel = Inputs. radio (
["Level 1: Basic Routing" , "Level 2: Wildcards" , "Level 3: QoS + LWT" ],
{label : "Select Difficulty:" , value : "Level 1: Basic Routing" }
)
viewof startGame = Inputs. button ("Start New Round" )
gameState = {
const levels = {
"Level 1: Basic Routing" : {
publishers : [
{id : "TempSensor" , topic : "home/bedroom/temp" , message : "22.5" },
{id : "DoorSensor" , topic : "home/entrance/door" , message : "open" },
{id : "LightControl" , topic : "home/bedroom/light" , message : "ON" }
],
subscribers : [
{id : "BedroomApp" , topics : ["home/bedroom/temp" , "home/bedroom/light" ]},
{id : "SecurityApp" , topics : ["home/entrance/door" ]},
{id : "LogServer" , topics : ["home/bedroom/temp" ]}
]
},
"Level 2: Wildcards" : {
publishers : [
{id : "Kitchen" , topic : "home/kitchen/temp" , message : "24" },
{id : "Garden" , topic : "garden/moisture" , message : "45%" },
{id : "Garage" , topic : "home/garage/motion" , message : "detected" }
],
subscribers : [
{id : "HomeHub" , topics : ["home/+/temp" ]},
{id : "AllSensors" , topics : ["#" ]},
{id : "GardenApp" , topics : ["garden/#" ]}
]
},
"Level 3: QoS + LWT" : {
publishers : [
{id : "SmartLock" , topic : "home/door/lock" , message : "locked" , qos : 2 },
{id : "Battery" , topic : "sensor/battery" , message : "15%" , qos : 1 },
{id : "Heartbeat" , topic : "device/status" , message : "online" , retained : true }
],
subscribers : [
{id : "SecurityPanel" , topics : ["home/door/+" ], qos : 2 },
{id : "Dashboard" , topics : ["sensor/#" , "device/#" ]},
{id : "AlertSystem" , topics : ["sensor/battery" ]}
]
}
};
const level = levels[gameLevel];
const randomPub = level. publishers [Math . floor (Math . random () * level. publishers . length )];
return {
level : gameLevel,
currentMessage : randomPub,
expectedRoutes : level. subscribers . filter (sub => {
return sub. topics . some (pattern => {
if (pattern === "#" ) return true ;
if (pattern. includes ("+" )) {
const parts = pattern. split ("/" );
const topicParts = randomPub. topic . split ("/" );
if (parts. length !== topicParts. length ) return false ;
return parts. every ((p, i) => p === "+" || p === topicParts[i]);
}
if (pattern. endsWith ("/#" )) {
return randomPub. topic . startsWith (pattern. replace ("/#" , "" ));
}
return pattern === randomPub. topic ;
});
}). map (s => s. id )
};
}
md `### Current Round: ${ gameState. level }
**Incoming Message:**
- **Publisher:** ${ gameState. currentMessage . id }
- **Topic:** \`${ gameState. currentMessage . topic }\`
- **Payload:** " ${ gameState. currentMessage . message } "
${ gameState. currentMessage . qos ? `- **QoS:** ${ gameState. currentMessage . qos } ` : "" }
${ gameState. currentMessage . retained ? "- **Retained:** Yes" : "" }
**Question:** Which subscribers should receive this message?
<details>
<summary>Click to reveal answer</summary>
**Correct answer:** ${ gameState. expectedRoutes . join (", " ) || "No matching subscribers" }
${ gameState. level . includes ("Wildcards" ) ? `
**Wildcard Matching Rules:**
- \` + \` matches exactly one level
- \` # \` matches zero or more levels (must be last)
- Topic: \`${ gameState. currentMessage . topic }\`
` : "" }
${ gameState. level . includes ("QoS" ) ? `
**QoS Considerations:**
- QoS 2 requires exactly-once delivery handshake
- Retained messages are stored by broker
- LWT triggers on unexpected disconnect
` : "" }
</details>`
Practice Exercises
Exercise 1: Multi-Room Temperature Monitor with CSV Logging
Objective : Extend Lab 1 by adding persistent data logging and multi-room alert thresholds.
Tasks :
Configure two ESP32 publishers for different rooms (living room and bedroom) using distinct topic hierarchies
Create a Python subscriber that logs all readings to a timestamped CSV file
Implement per-room alert thresholds (e.g., bedroom max 25C, living room max 30C)
Add a wildcard subscription (home/+/temperature) to receive from all rooms with a single subscriber
Exercise 2: Secure MQTT with TLS/SSL
Objective : Implement production-grade MQTT security with encryption.
Tasks :
Generate self-signed certificate for local Mosquitto broker
Configure Mosquitto for TLS on port 8883
Create Python MQTT client with TLS
Test that unencrypted connections are rejected
Exercise 3: MQTT-to-Database Pipeline
Objective : Build a complete data ingestion pipeline from MQTT to persistent storage.
Tasks :
Set up SQLite database for time-series sensor data
Create MQTT subscriber that writes to database
Simulate 5 different sensors publishing various metrics
Query database for average temperature over last hour
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.
What’s Next
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