Callback Function Handling
Callback Function Handling
Overview
Section titled “Overview”This section covers the MQTT callback function, which processes incoming messages on the ESP32. By the end of this section, you will be able to:
- Understand the callback function signature and parameters
- Implement topic-based message routing
- Handle different payload types (string, numeric, JSON)
- Manage state changes triggered by MQTT commands
Prerequisites
Section titled “Prerequisites”- MQTT client setup (01-10)
- JSON message handling (01-13)
- Basic Sketch architecture (01-11)
Key Concepts
Section titled “Key Concepts”The Callback Function
Section titled “The Callback Function”The MQTT callback function is registered via client.setCallback() and is invoked automatically when a message arrives on a subscribed topic.
Function Signature:
void callback(char* topic, byte* payload, unsigned int length);| Parameter | Type | Description |
|---|---|---|
topic | char* | The MQTT topic (null-terminated string) |
payload | byte* | Raw message bytes |
length | unsigned int | Length of the payload in bytes |
Important: The callback executes in the context of client.loop(). Keep it fast — do not use delay() or long-running operations inside the callback. If you need to run a long operation, set a flag and execute in the main loop().
How the Callback Works
Section titled “How the Callback Works”[MQTT Broker] --message--> [PubSubClient] --callback--> [Your Code] │ client.loop() (called every iteration)When client.loop() runs:
- It checks for incoming data on the MQTT socket
- If a complete message has arrived, it extracts the topic and payload
- It calls the registered callback function
- Your callback processes the message
Implementation Steps
Section titled “Implementation Steps”Step 1: Basic Callback Structure
Section titled “Step 1: Basic Callback Structure”#include <WiFi.h>#include <PubSubClient.h>
WiFiClient wifiClient;PubSubClient client(wifiClient);
void setup() { Serial.begin(115200);
// ... Wi-Fi connection ...
client.setServer("192.168.1.100", 1883); client.setCallback(callback); // Register the callback client.connect("esp32-callback");
// Subscribe to topics client.subscribe("esp32/control/#"); // Multi-level wildcard}
void loop() { if (!client.connected()) { // Reconnect } client.loop(); // Process incoming messages → triggers callback}
// The callback functionvoid callback(char* topic, byte* payload, unsigned int length) { // Step 1: Convert payload to String String message = ""; for (unsigned int i = 0; i < length; i++) { message += (char)payload[i]; }
// Step 2: Log the incoming message Serial.print("Message received ["); Serial.print(topic); Serial.print("]: "); Serial.println(message);
// Step 3: Route based on topic if (String(topic) == "esp32/control/led") { if (message == "ON") { digitalWrite(2, HIGH); } else if (message == "OFF") { digitalWrite(2, LOW); } }}Step 2: Topic-Based Routing with Multiple Topics
Section titled “Step 2: Topic-Based Routing with Multiple Topics”For projects with many subscribed topics, use a structured routing approach:
void callback(char* topic, byte* payload, unsigned int length) { // Convert payload String message = ""; for (unsigned int i = 0; i < length; i++) { message += (char)payload[i]; }
String topicStr = String(topic);
// Route by topic prefix if (topicStr.startsWith("esp32/control/led")) { handleLEDCommand(topicStr, message); } else if (topicStr.startsWith("esp32/control/sensor")) { handleSensorCommand(message); } else if (topicStr.startsWith("esp32/control/config")) { handleConfigCommand(message); } else { Serial.print("Unknown topic: "); Serial.println(topicStr); }}
void handleLEDCommand(String topic, String message) { // Parse the specific LED sub-topic if (topic.endsWith("/brightness")) { int brightness = message.toInt(); ledcWrite(0, brightness); Serial.print("LED brightness set to: "); Serial.println(brightness); } else if (topic.endsWith("/state")) { if (message == "ON") { digitalWrite(2, HIGH); } else { digitalWrite(2, LOW); } }}
void handleSensorCommand(String message) { int interval = message.toInt(); if (interval >= 1000 && interval <= 60000) { sensorInterval = interval; Serial.print("Sensor interval changed to: "); Serial.print(interval); Serial.println(" ms"); }}
void handleConfigCommand(String message) { // Parse JSON config // See 01-13 for ArduinoJson parsing StaticJsonDocument<200> doc; DeserializationError error = deserializeJson(doc, message);
if (!error) { if (doc.containsKey("threshold")) { temperatureThreshold = doc["threshold"]; } if (doc.containsKey("mode")) { const char* mode = doc["mode"]; Serial.print("Mode changed to: "); Serial.println(mode); } }}Step 3: Using Global State Flags (Non-Blocking Pattern)
Section titled “Step 3: Using Global State Flags (Non-Blocking Pattern)”Avoid executing long operations inside the callback. Use flags instead:
// Global flags set by callback, checked in loop()volatile bool ledToggleRequested = false;volatile bool sensorResetRequested = false;volatile int newPublishInterval = -1;
void callback(char* topic, byte* payload, unsigned int length) { String message = ""; for (unsigned int i = 0; i < length; i++) { message += (char)payload[i]; }
String topicStr = String(topic);
// Set flags (fast — doesn't block MQTT) if (topicStr == "esp32/control/led/toggle") { ledToggleRequested = true; // Will be processed in loop() } else if (topicStr == "esp32/control/reset") { sensorResetRequested = true; } else if (topicStr == "esp32/control/interval") { newPublishInterval = message.toInt(); // Will be applied in loop() }}
void loop() { client.loop();
// Process flags (may take time, that's OK here) if (ledToggleRequested) { ledToggleRequested = false; digitalWrite(2, !digitalRead(2)); Serial.println("LED toggled via flag"); }
if (sensorResetRequested) { sensorResetRequested = false; resetSensorReadings(); Serial.println("Sensor reset executed"); }
if (newPublishInterval > 0) { PUBLISH_INTERVAL = newPublishInterval; Serial.printf("Publish interval changed to %d ms\n", PUBLISH_INTERVAL); newPublishInterval = -1; }
// ... normal loop code ...}Step 4: Handling Large Payloads
Section titled “Step 4: Handling Large Payloads”For JSON payloads larger than 256 bytes, process differently:
void callback(char* topic, byte* payload, unsigned int length) { // For large payloads, use DynamicJsonDocument // Copy payload into a null-terminated buffer char* jsonBuffer = new char[length + 1]; memcpy(jsonBuffer, payload, length); jsonBuffer[length] = '\0';
// Parse DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, jsonBuffer);
if (!error) { const char* command = doc["command"]; JsonArray values = doc["values"];
Serial.print("Command: "); Serial.println(command); Serial.print("Values count: "); Serial.println(values.size()); }
delete[] jsonBuffer; // Free memory}Step 5: Callback with Class Methods (Object-Oriented)
Section titled “Step 5: Callback with Class Methods (Object-Oriented)”For advanced projects using classes:
class MQTTHandler { private: PubSubClient* client;
public: MQTTHandler(PubSubClient* mqttClient) : client(mqttClient) { client->setCallback(std::bind(&MQTTHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); }
void onMessage(char* topic, byte* payload, unsigned int length) { // This is now a class method — can access member variables String message = ""; for (int i = 0; i < length; i++) message += (char)payload[i];
Serial.printf("[%s] %s\n", topic, message.c_str());
// Process based on topic processCommand(String(topic), message); }
void processCommand(String topic, String message) { // Class-level processing }};Verification
Section titled “Verification”- Callback is invoked when a message arrives on a subscribed topic
- Topic-based routing correctly directs messages to the right handler
- Global flags set in the callback are processed in the main loop
- Large JSON payloads are parsed successfully
- The callback does not block
client.loop()processing
Troubleshooting
Section titled “Troubleshooting”Callback not being called
Section titled “Callback not being called”Causes:
client.setCallback(callback)not called beforeclient.connect()client.loop()not called in the main loop- Topic subscription failed
Solutions:
- Verify
setCallback()is called beforeconnect()insetup() - Add Serial print inside the callback to confirm it’s reached
- Call
client.loop()at the beginning ofloop(), not after delays - Use MQTT Explorer to verify the topic and payload are correct
Callback receives garbled data
Section titled “Callback receives garbled data”Cause: Payload contains null bytes or binary data.
Solution: When converting the payload to a string, check for null bytes:
String message = "";for (unsigned int i = 0; i < length; i++) { if (payload[i] != '\0') { // Skip null bytes message += (char)payload[i]; }}Multiple callbacks fire for one message
Section titled “Multiple callbacks fire for one message”Cause: Multiple client.loop() calls in the same loop iteration.
Solution: Ensure client.loop() is called exactly once per loop() iteration. Remove any additional calls.
Best Practices
Section titled “Best Practices”- Keep callbacks fast: Never use
delay(), long loops, or blocking operations inside callbacks - Use flag variables for slow operations: Set a flag in the callback, execute the operation in
loop() - Always convert payload before routing: Convert
byte*toStringonce, not in every handler - Log incoming messages with Serial: Helps diagnose issues during development
- Use topic prefixes for organization:
project/device/actionstructure makes routing cleaner - Consider using
volatilefor flags: When setting flags from callback to be read inloop()
Summary
Section titled “Summary”- The callback function receives topic, payload, and length — convert payload to String first
- Topic-based routing directs messages to specific handler functions
- Use global flags for long operations to avoid blocking
client.loop() - Handle large payloads with DynamicJsonDocument on the heap
- The callback must execute quickly — it runs inside the MQTT processing path
- Class-based callbacks are possible with
std::bindfor object-oriented designs