Learning Goals 5 min
Publishing is half the story. The other half: react when someone (a phone app, Home Assistant, or another ESP) publishes to a topic you care about. By the end of this lesson you will:
- Subscribe to one or more topics with the
subscribe()+ callback pattern. - Parse incoming payloads (plain text and JSON) and dispatch to the right action.
- Drive a real LED on the ESP from MQTT Explorer or another publisher.
Warm-Up 10 min
Hardware from yesterday + LED + 220 Ω resistor on D5 (PWM-capable).
The subscribe pattern
PubSubClient delivers incoming messages via a single callback function you register. The callback receives: topic name, payload bytes, payload length. You parse and react.
New Concept · Callback dispatch 25 min
Set the callback
void onMqttMessage(char* topic, byte* payload, unsigned int length);
void setup() {
// ... wifi + setServer
mqtt.setCallback(onMqttMessage);
}Subscribe after connect
bool connectMQTT() {
if (mqtt.connect(CLIENT_ID, TOPIC_STATUS, 0, true, "offline")) {
mqtt.publish(TOPIC_STATUS, "online", true);
mqtt.subscribe("advaslearning/aliya/lamp/cmd");
mqtt.subscribe("advaslearning/aliya/lamp/brightness");
return true;
}
return false;
}You must re-subscribe after every reconnect (the broker forgets subscriptions on disconnect).
The callback
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
// 1. Make payload null-terminated so we can treat it as a string
char msg[64];
unsigned int n = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
memcpy(msg, payload, n);
msg[n] = 0;
Serial.print("# rx ["); Serial.print(topic); Serial.print("] "); Serial.println(msg);
// 2. Dispatch on topic
if (strcmp(topic, "advaslearning/aliya/lamp/cmd") == 0) {
if (strcmp(msg, "ON") == 0) digitalWrite(LED_PIN, HIGH);
else if (strcmp(msg, "OFF") == 0) digitalWrite(LED_PIN, LOW);
}
else if (strcmp(topic, "advaslearning/aliya/lamp/brightness") == 0) {
int v = atoi(msg);
v = constrain(v, 0, 255);
analogWrite(LED_PIN, v);
}
}Note: payload is NOT null-terminated. Always copy + terminate yourself before strcmp or atoi.
JSON payloads
#include <ArduinoJson.h>
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
StaticJsonDocument<128> doc;
DeserializationError err = deserializeJson(doc, payload, length);
if (err) { Serial.println("# bad json"); return; }
if (strcmp(topic, "advaslearning/aliya/lamp/set") == 0) {
bool powerOn = doc["on"] | false;
int brightness = doc["brightness"] | 128;
digitalWrite(LED_PIN, powerOn ? HIGH : LOW);
// For combined ON+brightness: analogWrite when on, 0 when off.
}
}The | operator on JsonDocument fields provides a default if the field is missing.
Publish a state-acknowledgement
After acting on a command, publish back to a .../state topic so the dashboard knows what actually happened:
mqtt.publish("advaslearning/aliya/lamp/state",
powerOn ? "ON" : "OFF",
true);Two-topic pattern is standard: /set for commands going down, /state for state going up.
Worked Example · MQTT-controlled LED 25 min
Step 1 — sketch
// L04-15 · MQTT-controlled LED
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include <PubSubClient.h>
const char* SSID = "YourNetwork";
const char* PASSWORD = "YourPassword";
const char* MQTT_SERVER = "test.mosquitto.org";
const char* CLIENT_ID = "advaslearning-aliya-lamp1";
const char* TOPIC_CMD = "advaslearning/aliya/lamp/cmd";
const char* TOPIC_BRIGHT = "advaslearning/aliya/lamp/brightness";
const char* TOPIC_STATE = "advaslearning/aliya/lamp/state";
const char* TOPIC_STATUS = "advaslearning/aliya/lamp/status";
const int LED_PIN = 14; // D5 on NodeMCU
WiFiClient netClient;
PubSubClient mqtt(netClient);
bool powerOn = false;
int brightness = 128;
void apply() {
analogWrite(LED_PIN, powerOn ? brightness : 0);
mqtt.publish(TOPIC_STATE, powerOn ? "ON" : "OFF", true);
}
void onMessage(char* topic, byte* payload, unsigned int length) {
char msg[32];
unsigned int n = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
memcpy(msg, payload, n);
msg[n] = 0;
Serial.printf("# rx [%s] %s\n", topic, msg);
if (strcmp(topic, TOPIC_CMD) == 0) {
if (strcmp(msg, "ON") == 0) powerOn = true;
else if (strcmp(msg, "OFF") == 0) powerOn = false;
apply();
}
else if (strcmp(topic, TOPIC_BRIGHT) == 0) {
int v = atoi(msg);
brightness = constrain(v, 0, 255);
if (brightness > 0) powerOn = true;
apply();
}
}
bool connectMQTT() {
Serial.print("# MQTT...");
if (mqtt.connect(CLIENT_ID, TOPIC_STATUS, 0, true, "offline")) {
Serial.println(" OK");
mqtt.publish(TOPIC_STATUS, "online", true);
mqtt.subscribe(TOPIC_CMD);
mqtt.subscribe(TOPIC_BRIGHT);
return true;
}
Serial.print(" failed rc="); Serial.println(mqtt.state());
return false;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println();
mqtt.setServer(MQTT_SERVER, 1883);
mqtt.setCallback(onMessage);
}
void loop() {
if (!mqtt.connected()) { connectMQTT(); delay(500); return; }
mqtt.loop();
}Step 2 — test from MQTT Explorer
- Open MQTT Explorer, connected to test.mosquitto.org.
- Subscribe to
advaslearning/aliya/lamp/#. - You see the ESP's
status: onlineand currentstate: OFF. - Click "publish". Topic =
advaslearning/aliya/lamp/cmd. Payload =ON. Click Send. - Your LED lights up. The
statetopic updates toON. - Publish
50to the brightness topic. The LED dims to ~20%. - Publish
OFFto cmd. LED off.
Step 3 — two ESPs talking
Upload a publishing sketch (from L04-14) to a second board. Set its publish topic = your lamp's cmd topic, payload alternating ON/OFF every 5 s. Watch the lamp obey the second board. Two ESPs, no wires between them, full LAN-wide control.
Step 4 — verify the LWT
Unplug. After 30 s, status goes to offline. Plug back → online.
Step 5 — Home Assistant preview (just look)
Home Assistant (free, runs on Raspberry Pi or a laptop) can auto-discover MQTT devices that follow a specific topic convention. The two-topic cmd/state pattern you just built is exactly what Home Assistant expects. Tomorrow we'll wire it up properly.
Try It Yourself 15 min
Goal: Add a /set JSON topic that combines power + brightness in one message: {"on": true, "brightness": 128}.
Goal: Use the L02-26 smart bin lid pattern. Subscribe to .../timer; payload is a number of minutes; turn the lamp on for that many minutes then off automatically. Use millis() not delay.
Goal: Wildcard subscription. Subscribe to advaslearning/aliya/# on a debug ESP. Print every message that flows in your namespace. Useful for hunting bugs.
Mini-Challenge · Two-button publisher 10 min
Wire two physical buttons on a separate ESP. Button A publishes ON to your lamp's cmd topic; Button B publishes OFF. Your lamp ESP reacts. You've built a wireless remote for the lamp — no app needed.
Recap 5 min
Subscribe to topics + register a callback + dispatch on topic in the callback. Two-topic pattern (cmd → device, state → broker) for every actuator. Re-subscribe on reconnect. Don't block in the callback. Tomorrow: Cluster C's capstone — a real Home Assistant sensor node.
- Subscribe
- Tell the broker "send me every message published to this topic".
- Callback
- Function the library calls when a subscribed message arrives. Args: topic, payload bytes, length.
- Two-topic pattern (cmd/state)
- One topic for "tell me what to do" (subscribed by the device), one for "here's what I did" (published by the device). Standard in smart-home.
- Null-termination
- MQTT payloads are NOT null-terminated. Copy to a buffer and add
\0before treating as a C string. - Re-subscribe on reconnect
- Subscription state lives in the broker per-connection. After any reconnect, your
connectMQTTmust re-subscribe. - JSON payload
- Structured commands like
{"on": true, "brightness": 128}. Parse with ArduinoJson. - Wildcard subscription
- Subscribe to
+(single level) or#(multi-level) to receive all matching topics in one callback. - Status / availability topic
- The retained
online/offlinetopic that says whether the device is alive. LWT auto-publishesofflinewhen the device disconnects.
Homework 5 min
- Build the MQTT-controlled lamp + 2-button remote. Video both responding.
- Read ahead to ARD-L04-16 (Home Assistant Sensor Node). Bring a Raspberry Pi with Home Assistant if you have one — otherwise we'll cover the concepts and you can revisit later.