Deploy an end-to-end publish/subscribe network connecting an ESP32 to a local Mosquitto broker using QoS levels and LWT status flags.
Domain
MQTT Networking
Difficulty
⭐⭐⭐☆☆ (Intermediate)
Course Module
IoT Networking
Deliverables
ESP32 Firmware, Python Controller Script
1. MQTT Broker & Client Topology
To enable bi-directional data flow without direct socket management, we use the lightweight MQTT protocol. The architecture below details the layout of our smart office pilot. The ESP32 edge node connects via Wi-Fi to the local Mosquitto broker running on the VirtualBox guest VM. It publishes temperature and light values to distinct sub-topics (`office/sensor1/temp` and `office/sensor1/light`) at QoS 0. It also subscribes to a command topic (`office/actuator/relay`) at QoS 1 to control a relay. A Python controller monitors these channels and publishes commands in response.
2. Part 1: Step-by-Step Individual Installation Commands & Library Configuration
Follow these detailed steps to load the required C++ and Python MQTT libraries, configure your connection credentials, and build the command-line publisher scripts.
STEP 1
Install the PubSubClient Library in the Arduino IDE
Open the Library Manager inside the Arduino IDE to download Nick O'Leary's MQTT library, which provides communication functions for the ESP32.
Arduino IDE GUI Steps:
1. Open the Arduino IDE.
2. In the left vertical menu, click the Library Manager icon (stacked books) or go to "Tools" -> "Manage Libraries...".
3. In the search input field, type "PubSubClient".
4. Locate the package titled "PubSubClient" by Nick O'Leary.
5. Click the "Install" button. Wait for the installation to complete.
We download Nick O'Leary's `PubSubClient` library to access standard API bindings for managing broker connection handshakes, publishing messages, and processing incoming subscriptions.
STEP 2
Launch Terminal and Create Network Project Folder
Boot up your VirtualBox Ubuntu machine. Open the terminal (Ctrl+Alt+T) and create a directory to organize your C++ and Python source files.
ubuntu@iot-vm:~$ mkdir -p ~/workspace/mqtt_network && cd ~/workspace/mqtt_network
We run `mkdir -p` and `cd` to generate and enter a dedicated workspace directory named `~/workspace/mqtt_network` to isolate our Python controller script.
STEP 3
Install Paho-MQTT Library inside the virtual environment
Create an isolated virtual environment and install the Eclipse Paho MQTT client library for Python.
We initialize the Python virtual environment and install `paho-mqtt` using pip. This library provides Python bindings for subscribing to telemetry topics and publishing actuator commands.
STEP 4
Create the Python Controller Script
Open the graphical text editor in the background to write the serial listener script.
We invoke `gedit` alongside the `&` symbol to open the graphical text editor in a background thread, leaving our terminal terminal prompt active for running commands.
STEP 5
Verify the Local IP Address of the VM
Locate the IP address of your virtual machine's bridged interface, which you will use in the ESP32 code configuration.
(env) ubuntu@iot-vm:~/workspace/mqtt_network$ ip a show dev enp0s3 | grep inet
We run `ip a` to check the IP address of interface `enp0s3`, which represents the VM's bridged connection on the local network.
STEP 6
Configure and Upload the ESP32 Firmware
Edit the firmware variables in the Arduino IDE to match your home Wi-Fi SSID, password, and the local IP address of your virtual machine.
Firmware Settings (Arduino IDE):
1. Locate lines 10-12 in your sketch editor.
2. Replace "YOUR_WIFI_SSID" with your actual local router name.
3. Replace "YOUR_WIFI_PASSWORD" with your actual Wi-Fi password.
4. Replace the "mqtt_server" IP string with your VM's bridged IP address (found in Step 5).
5. Click the circular Upload arrow icon in the toolbar to upload the code to the ESP32.
We configure Wi-Fi credentials and the broker IP in the firmware to ensure the ESP32 can connect to your local network and target the VM's Mosquitto broker on port 1883.
STEP 7
Run the Python Controller Script
Run the controller script inside the VM terminal to start monitoring telemetry data and commanding the actuators.
We execute the Python controller script, starting the loop that subscribes to sensor topics and publishes "ON" or "OFF" commands to the relay topic in response to temperature changes.
3. Automated Telemetry & Control Loop Pipeline
The system operates in a continuous control loop: the ESP32 publishes sensor readings to the broker, the Python controller processes the data, and sends back commands to trigger the relay. The workflow below maps this sequence.
4. Part 2: Complete Codebases & Line-by-Line Breakdowns
Below is the complete C++ firmware code for the ESP32 node, followed by the Python script to run on your VirtualBox VM to complete the control loop.
#include <WiFi.h>: Imports the ESP32 Wi-Fi library, enabling functions to configure client profiles and connect to access points.
#include <PubSubClient.h>: Imports the MQTT client library to manage broker connections and message transactions.
WiFiClient espClient;: Instantiates a Wi-Fi client object to handle TCP socket connections.
PubSubClient client(espClient);: Instantiates the MQTT client, linking it to the Wi-Fi client wrapper.
setup_wifi(): Connects the ESP32 to your local Wi-Fi router. Loops until a local IP address is successfully assigned.
client.setServer(mqtt_server, 1883);: Registers the broker IP address and target port (1883) in the client configuration.
client.setCallback(callback);: Registers the callback function that will run when messages are received on subscribed topics.
client.connect("ESP32_SensorNode", "office/sensor1/status", 0, true, "offline"): Connects to the broker, registering "ESP32_SensorNode" as the client ID. Sets up the Last Will and Testament (LWT) topic (`office/sensor1/status`, QoS 0, Retained) to publish "offline" if the connection is lost.
client.publish("office/sensor1/status", "online", true);: Publishes an "online" message to the status topic on boot, setting the Retain flag to `true` to keep the message cached on the broker.
client.subscribe("office/actuator/relay", 1);: Subscribes to the relay command topic at QoS 1, ensuring the broker delivers command messages even if connection drops briefly.
client.loop();: Runs the client background process. This must be called regularly in the main loop to handle keep-alive pings and incoming message buffers.
// Practice Project 5: ESP32 MQTT Client Firmware Code#include<WiFi.h>#include<PubSubClient.h>// Wi-Fi and Broker Connection Constantsconstchar* ssid = "YOUR_WIFI_SSID";
constchar* password = "YOUR_WIFI_PASSWORD";
constchar* mqtt_server = "192.168.1.120"; // Replace with your VM's Bridged IP AddressconstintRELAY_PIN = 12; // GPIO Pin connected to the actuator relayWiFiClientespClient;
PubSubClientclient(espClient);
unsignedlonglastMsgTime = 0;
floatdummyTemp = 24.0; // Simulated temperature sensor readingintdummyLight = 450; // Simulated LDR analog light readingvoidsetup_wifi() {
delay(10);
Serial.print("Connecting to Wi-Fi SSID: "); Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi connection established. IP Address: ");
Serial.println(WiFi.localIP());
}
voidcallback(char* topic, byte* payload, unsignedintlength) {
Serial.print("Message received on topic ["); Serial.print(topic); Serial.print("] Payload: ");
Stringmessage;
for (inti = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.println(message);
// Trigger Relay state in response to commandsif (message == "ON") {
digitalWrite(RELAY_PIN, HIGH); // Turn on Relay
} elseif (message == "OFF") {
digitalWrite(RELAY_PIN, LOW); // Turn off Relay
}
}
voidreconnect() {
while (!client.connected()) {
Serial.print("Connecting to local MQTT Broker...");
// Connect with Last Will and Testament configured (LWT)if (client.connect("ESP32_SensorNode", "office/sensor1/status", 0, true, "offline")) {
Serial.println("Connected.");
// Publish status and subscribe to relay commandsclient.publish("office/sensor1/status", "online", true);
client.subscribe("office/actuator/relay", 1); // Subscribe with QoS level 1
} else {
Serial.print("Connection failed, rc=");
Serial.print(client.state());
Serial.println(" Waiting 5 seconds before retrying...");
delay(5000);
}
}
}
voidsetup() {
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
voidloop() {
if (!client.connected()) {
reconnect();
}
client.loop();
unsignedlongnow = millis();
if (now - lastMsgTime > 5000) { // Publish telemetry every 5 secondslastMsgTime = now;
// Generate simulated variationsdummyTemp += ((float)random(-50, 50) / 100.0);
dummyLight += random(-20, 20);
StringtempPayload = String(dummyTemp, 2);
StringlightPayload = String(dummyLight);
client.publish("office/sensor1/temp", tempPayload.c_str());
client.publish("office/sensor1/light", lightPayload.c_str());
Serial.println("Telemetry data published successfully.");
}
}
import paho.mqtt.client as mqtt: Imports the Eclipse Paho MQTT client module for Python.
mqtt.Client(): Instantiates the Python MQTT client object to manage network loops.
client.on_connect = on_connect: Registers the callback function to handle successful connections to the broker.
client.on_message = on_message: Registers the callback function that runs when published messages are received on subscribed topics.
client.subscribe("office/sensor1/temp"): Subscribes to the temperature topic to monitor incoming data from the ESP32.
client.publish("office/actuator/relay", "ON", qos=1): Publishes an "ON" command to the relay topic at QoS 1, ensuring the command is delivered to activate the cooling fan.
client.loop_forever(): Starts the client event loop. This blocks execution, handling network reconnects and processing incoming payloads indefinitely.
# Practice Project 5: Python Network Controller Scriptimportpaho.mqtt.clientasmqttimporttime# Connection Callbackdefon_connect(client, userdata, flags, rc):
print(f"Connected to local broker with status code: {rc}")
# Subscribe to both sensor topics using single-level wildcardsclient.subscribe("office/sensor1/+")
print("Subscribed to topic: office/sensor1/+")
# Message Processing Callbackdefon_message(client, userdata, msg):
topic = msg.topicpayload = msg.payload.decode('utf-8')
print(f"[MQTT IN] {topic} : {payload}")
# Check temperature thresholdif"temp"intopic:
try:
temp_val = float(payload)
# Decision logic: if temperature exceeds 26.0°C, turn relay ONiftemp_val > 26.0:
print(" -> Temperature high! Sending ON command to relay...")
client.publish("office/actuator/relay", "ON", qos=1)
else:
print(" -> Temperature normal. Sending OFF command to relay...")
client.publish("office/actuator/relay", "OFF", qos=1)
exceptValueError:
print("Error: Failed to parse float value from payload.")
defmain():
broker_ip = "127.0.0.1"// Runs locally on the VMport = 1883
client = mqtt.Client()
client.on_connect = on_connectclient.on_message = on_messageprint(f"Attempting connection to local MQTT broker at {broker_ip}...")
client.connect(broker_ip, port, 60)
# Start block network handler loopclient.loop_forever()
if__name__ == "__main__":
main()
5. Deliverables Summary
Created Artifacts
ESP32 firmware code with Wi-Fi/MQTT client configuration: mqtt_client.ino.
Topic design document outlining namespace structure and QoS selections.
Verification Proof
Screenshot of the VM terminal running controller.py, displaying incoming telemetry logs and generated command publish events.
A video clip showing the ESP32 serial console connection logging Wi-Fi connection states, broker connection states, and active-LOW relay transitions.
6. Closing Explanation: Why We Did This & What It Accomplishes
Architectural Intent & Operational Impact
Why We Did This
Choosing QoS 0 for sensor updates reduces network traffic by publishing telemetry data frequently without requiring delivery handshakes.
Choosing QoS 1 for actuator commands guarantees message delivery, ensuring that control instructions (like turning off a heater) are always received by the edge device.
Configuring a Last Will and Testament (LWT) topic allows the broker to automatically flag a device as "offline" if its connection is lost, enabling real-time status monitoring.
What This Accomplishes
Establishes a bi-directional messaging network over standard TCP/IP sockets, removing the need for physical serial cable connections to edge devices.
Deploys an automated local control loop, showing how sensor readings can trigger remote actuator states across a network.