Chapter 106: MQTT Topic Design Patterns

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the fundamental importance of well-structured MQTT topics in IoT systems.
  • Identify and explain common MQTT topic design patterns.
  • Design MQTT topics that promote scalability, security, and ease of use.
  • Implement logical and hierarchical topic structures for various IoT application scenarios.
  • Effectively utilize MQTT wildcards (+ and #) for flexible topic subscriptions.
  • Appreciate the nuances of topic design across different ESP32 variants.

Introduction

In the previous chapters, we explored the MQTT protocol, how to implement an MQTT client on the ESP32, manage Quality of Service (QoS), and secure communications. Now, we turn our attention to a critical aspect of any MQTT-based IoT system: the design of MQTT topics.

MQTT topics are the “addresses” to which messages are published and from which messages are subscribed. A well-thought-out topic structure is akin to a well-organized filing system; it makes data easy to find, manage, and secure. Conversely, a poorly designed topic structure can lead to confusion, inefficiencies, and difficulties in scaling your IoT solution. This chapter will introduce you to common patterns and best practices for designing MQTT topics, ensuring your ESP32 applications communicate effectively and your overall system remains manageable as it grows.

Theory

MQTT Topic Fundamentals Recap

Before diving into design patterns, let’s briefly revisit the core characteristics of MQTT topics:

  • UTF-8 String: Topics are simple UTF-8 strings.
  • Case-Sensitive: MyTopic and mytopic are considered different topics.
  • Hierarchical Structure: Topics are structured hierarchically using the forward slash (/) as a level separator (e.g., home/livingroom/temperature). This allows for logical organization of information.
  • No Leading/Trailing Slashes: While some brokers might tolerate them, it’s best practice to avoid topics that start or end with a /. For example, use sensors/temperature instead of /sensors/temperature/.
  • No Empty Levels: A topic like home//temperature (with an empty level) is invalid.

MQTT Wildcards

MQTT provides two wildcard characters for subscribing to multiple topics simultaneously. Understanding these is crucial for designing effective topic structures that clients can flexibly subscribe to.

  • Single-Level Wildcard (+): This wildcard matches any single topic level.
    • Example: sensors/+/temperature would match:
      • sensors/livingroom/temperature
      • sensors/bedroom/temperature
      • But NOT sensors/kitchen/fridge/temperature (because + only covers one level)
      • And NOT sensors/temperature (because a level must exist for the + to match)
  • Multi-Level Wildcard (#): This wildcard matches multiple topic levels at the end of a topic string. It must be the last character in the topic string used for subscription.
    • Example: sensors/kitchen/# would match:
      • sensors/kitchen/temperature
      • sensors/kitchen/fridge/door_status
      • sensors/kitchen/humidity/sensor1
    • Example: # by itself subscribes to all messages (generally used only for debugging or specific administrative purposes due to high traffic potential).
Feature Description Example / Note
Topic Fundamentals
Format UTF-8 string. Allows for international characters, but stick to simple ASCII for broader compatibility if unsure.
Case Sensitivity Topics are case-sensitive. home/LivingRoom/Temp is different from home/livingroom/temp. Consistency is key.
Hierarchy Uses forward slash (/) as a level separator. buildingA/floor1/room101/temperature (4 levels)
Leading/Trailing Slashes Avoid. Not recommended by MQTT specification. Use: sensors/temp
Avoid: /sensors/temp/
Empty Levels Invalid. Two consecutive slashes (//) mean an empty level. Invalid: home//temperature
Length Max length is 65,535 bytes, but keep them reasonably short for efficiency. Overly long topics consume more bandwidth and memory.
MQTT Wildcards (for Subscriptions)
Single-Level Wildcard (+) Matches any single topic level. Must occupy an entire level by itself. Subscription: sensors/+/temperature
Matches: sensors/livingroom/temperature, sensors/kitchen/temperature
Does NOT match: sensors/temperature, sensors/kitchen/fridge/temperature
Multi-Level Wildcard (#) Matches multiple topic levels at the end of a topic. Must be the last character. Can also match “none” if the parent level is valid. Subscription: sensors/kitchen/#
Matches: sensors/kitchen/temperature, sensors/kitchen/fridge/door, sensors/kitchen (if broker supports matching parent)
Subscription: # matches all topics (use with caution).
graph LR
    subgraph "Example Topics Published"
        T1["buildingA/floor1/room101/temp"]
        T2["buildingA/floor1/room102/temp"]
        T3["buildingA/floor2/room201/humidity"]
        T4["buildingA/floor1/room101/light"]
        T5["buildingB/floor1/room101/temp"]
    end

    subgraph "Example Subscriptions & Matches"
        direction LR
        S1["<b>Subscription 1:</b><br>buildingA/floor1/<b>+</b>/temp"] --> M1_1{"Matches T1"}
        S1 --> M1_2{"Matches T2"}
        S1 -.-> NM1_3{"Does NOT Match T3 (wrong metric)"}
        S1 -.-> NM1_4{"Does NOT Match T4 (wrong metric)"}
        S1 -.-> NM1_5{"Does NOT Match T5 (wrong building)"}

        S2["<b>Subscription 2:</b><br>buildingA/<b>#</b>"] --> M2_1{"Matches T1"}
        S2 --> M2_2{"Matches T2"}
        S2 --> M2_3{"Matches T3"}
        S2 --> M2_4{"Matches T4"}
        S2 -.-> NM2_5{"Does NOT Match T5 (wrong building)"}

        S3["<b>Subscription 3:</b><br>buildingA/floor1/room101/<b>#</b>"] --> M3_1{"Matches T1"}
        S3 --> M3_4{"Matches T4"}
        S3 -.-> NM3_2{"Does NOT Match T2 (wrong room)"}
    end

    classDef topic fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    classDef sub fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
    classDef match fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46
    classDef nomatch fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B

    class T1,T2,T3,T4,T5 topic;
    class S1,S2,S3 sub;
    class M1_1,M1_2,M2_1,M2_2,M2_3,M2_4,M3_1,M3_4 match;
    class NM1_3,NM1_4,NM1_5,NM2_5,NM3_2 nomatch;

    linkStyle 0,1,5,6,7,10,11 stroke-width:2px;
    linkStyle 2,3,4,8,9 stroke-width:2px,stroke:#DC2626,stroke-dasharray:5 5;

Key Considerations for Topic Design

When designing your MQTT topic namespace, several factors should influence your decisions:

  1. Hierarchy and Structure:
    • Organize topics logically, often reflecting the physical or functional layout of your system.
    • Start broad and get more specific (e.g., region/site/building/floor/room/device/sensor_type).
  2. Uniqueness:
    • Ensure that each publishing device or data point has a clearly identifiable and unique topic path. This is often achieved by including a unique device ID.
  3. Scalability:
    • Design topics with future growth in mind. Will your system expand to more devices, locations, or data types? The topic structure should accommodate this without requiring a complete overhaul.
  4. Security:
    • Topic design plays a role in security. MQTT brokers often allow access control lists (ACLs) to be defined based on topic patterns. A granular topic structure allows for fine-grained permissions (e.g., a device can only publish to its own designated topics: devices/DEVICE_ID/#).
  5. Readability and Maintainability:
    • Topics should be human-readable and easy to understand. This simplifies debugging, monitoring, and onboarding new developers.
    • Use consistent naming conventions (e.g., snake_case, camelCase, or kebab-case). snake_case is very common.
  6. Data vs. Command Topics:
    • It’s often beneficial to distinguish between topics used for publishing data (telemetry) and topics used for sending commands to devices.
    • Example:
      • Data: devices/esp32_xyz/telemetry/temperature
      • Command: devices/esp32_xyz/commands/reboot
  7. Payload Consideration:
    • While not strictly part of the topic string itself, consider what data the payload will contain. Sometimes, it’s better to have more specific topics with smaller, focused payloads rather than generic topics with large, complex JSON payloads that require parsing to extract relevant information.

Common MQTT Topic Design Patterns

There’s no single “correct” way to design MQTT topics, as the optimal structure depends heavily on the application. However, several common patterns have emerged:

Pattern Name Typical Structure Example Primary Focus / Use Case Example Topic
Device-Centric [class]/[id]/[metric_type] Individual devices and their specific data. Easy to isolate device data. env_sensors/sensor_001/temperature
Location-Centric [region]/[site]/[room]/[sensor] Physical location of devices/sensors. Ideal for smart buildings, asset tracking. usa/ca/office_A/room101/hvac_temp
Function-Centric (Feature-Centric) [system_function]/[group]/[id]/[action] Organizing by role or service provided, e.g., all lighting controls. lighting/living_room/main_light/command
Project/Tenant-Centric [tenant_id]/[user_id]/[device_id]/[data] Multi-tenant platforms requiring data isolation between customers/projects. acme_iot/cust_xyz/tracker_01/location
Data Type-Centric (Measurement-Centric) [data_type]/[location]/[device_id] Primary organization by type of data (e.g., all temperatures, all alerts). temperatures/office_b/sensor_005/value
Standardized (Cmd, Status, Telemetry, Event) [prefix]/[id]/telemetry/[metric]
[prefix]/[id]/command/[action]
[prefix]/[id]/status/[param]
[prefix]/[id]/events/[type]
Clear, consistent structure for device interaction. Highly recommended for most IoT projects. devices/esp32_abc/telemetry/humidity
devices/esp32_abc/command/reboot
Versioning in Topics [api_version]/[rest_of_topic] Managing breaking changes in topic structure or payload formats over time. v2/sensors/thermostat_01/settings
graph LR
    Root["[base_prefix]"] --> DeviceID["[device_id]"];
    subgraph "Device Interaction Categories"
        direction LR
        DeviceID --> Telemetry["telemetry"];
        Telemetry --> Temp["temperature<br/>(e.g., 23.5)"];
        Telemetry --> Humidity["humidity<br/>(e.g., 45.2)"];
        Telemetry --> OtherData["... (other sensor data)"];
        DeviceID --> Status["status"];
        Status --> Online["online<br/>(e.g., true/false)"];
        Status --> FirmwareVer["firmware_version<br/>(e.g., v1.2.0)"];
        Status --> OtherStatus["... (other status params)"];
        DeviceID --> Commands["command"];
        Commands --> Reboot["reboot<br/>(Payload: {})"];
        Commands --> SetValue["set_value<br/>(Payload: {target:led, value:ON})"];
        Commands --> OtherCmds["... (other commands)"];
        DeviceID --> Events["events"];
        Events --> ButtonPress["button_press<br/>(Payload: {type:single})"];
        Events --> LowBattery["low_battery<br/>(Payload: {level:15})"];
        Events --> OtherEvents["... (other event types)"];
        DeviceID --> Config["config"];
        Config --> ConfigSet["set<br/>(Device Subscribes)"];
        Config --> ConfigGet["get<br/>(Device Publishes/Responds)"];
    end
    classDef root fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
    classDef device fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E
    classDef category fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    classDef leaf fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46
    class Root root;
    class DeviceID device;
    class Telemetry,Status,Commands,Events,Config category;
    class Temp,Humidity,OtherData,Online,FirmwareVer,OtherStatus,Reboot,SetValue,OtherCmds,ButtonPress,LowBattery,OtherEvents,ConfigSet,ConfigGet leaf;
  • Using Versioning in Topics:
    • Structure: [api_version]/[rest_of_topic_structure]
    • Example: v1/sensors/temp_sensor_01/temperature
    • Example: v2/sensors/temp_sensor_01/telemetry (perhaps v2 uses a different payload format or topic structure beyond the version)
    • Use Case: Useful when you anticipate breaking changes in your topic structure or payload formats over time. Allows older devices to continue using v1 topics while newer devices or backend systems can migrate to v2.
    • Tip: Versioning can also be handled in the payload, but topic-level versioning is clearer when the structure itself changes.

Best Practices for Topic Design

  • Be Specific, Not Overly Verbose: Include enough detail for clarity, but avoid excessively long topic strings that consume unnecessary bandwidth and memory.
  • Consistency is Key: Choose a naming convention (e.g., snake_case, all lowercase) and stick to it.
  • Device ID: Almost always include a unique device identifier in your topics. This is crucial for distinguishing messages from different devices. The ESP32’s MAC address can be a good source for a unique ID.
  • Directionality: Clearly distinguish topics for data flowing from the device (telemetry, status) versus data flowing to the device (commands, configuration).
  • Avoid Using $SYS: Topics starting with $SYS/ are reserved for MQTT broker-specific statistics and information. Do not use them for your application data.
  • Plan for Wildcard Subscriptions: Design your hierarchy so that wildcard subscriptions are meaningful and efficient. For example, if you want to get all temperature data, a structure like .../temperature at the end is helpful.
  • Document Your Topic Structure: Maintain clear documentation of your topic namespace, including the purpose of each level and expected payload formats. This is invaluable for team collaboration and long-term maintenance.

Practical Examples

Let’s consider a practical scenario: a simple smart home system with an ESP32 acting as a sensor node. It measures temperature and humidity and can control an LED. We’ll use a device-centric pattern combined with command/telemetry separation.

Assumed Setup:

  • An MQTT broker is running and accessible (e.g., Mosquitto on a local machine, or a public broker like mqtt.eclipseprojects.io or test.mosquitto.org).
  • You have an MQTT client tool (e.g., MQTT Explorer, mosquitto_sub, mosquitto_pub) for observing messages and publishing commands.
  • Your ESP-IDF environment is set up, and you are familiar with building and flashing projects (as covered in earlier chapters).
  • You have a basic ESP32 project with the ESP-MQTT client initialized (refer to Chapter 102: MQTT Client Implementation with ESP-MQTT).

Topic Structure Design:

Let’s define our topic structure:

  • Device ID: We’ll use a placeholder esp32_livingroom_01. In a real application, this could be derived from the MAC address or a unique ID provisioned to the device.
  • Telemetry:
    • Temperature: smarthome/devices/esp32_livingroom_01/telemetry/temperature (Payload: e.g., {"value": 23.5, "unit": "C"})
    • Humidity: smarthome/devices/esp32_livingroom_01/telemetry/humidity (Payload: e.g., {"value": 45.2, "unit": "%"})
  • Status:
    • Online status (using Last Will and Testament – covered in Chapter 107): smarthome/devices/esp32_livingroom_01/status/online (Payload: true or false)
  • Commands:
    • LED control: smarthome/devices/esp32_livingroom_01/commands/set_led (Payload: e.g., {"state": "ON"} or {"state": "OFF"})
  • Acknowledgements (Optional but good practice):
    • Command acknowledgement: smarthome/devices/esp32_livingroom_01/commands/set_led/ack (Payload: e.g., {"status": "success", "original_command_id": "cmd123"})

Topic Breakdown:

Purpose / Data Point Full Topic Path
(Device ID: esp32_livingroom_01)
Direction Example Payload Notes
Temperature Reading smarthome/devices/esp32_livingroom_01/telemetry/temperature ESP32 → Broker {"value": 23.5, "unit": "C"} Device publishes its temperature data.
Humidity Reading smarthome/devices/esp32_livingroom_01/telemetry/humidity ESP32 → Broker {"value": 45.2, "unit": "%"} Device publishes its humidity data.
Online Status smarthome/devices/esp32_livingroom_01/status/online ESP32 → Broker true or false Often used with MQTT Last Will and Testament (LWT). Payload true on connect, LWT sets to false.
Set LED State (Command) smarthome/devices/esp32_livingroom_01/commands/set_led Broker → ESP32 {"state": "ON"} or {"state": "OFF"} ESP32 subscribes to this topic to receive commands.
LED Command Acknowledgement smarthome/devices/esp32_livingroom_01/commands/set_led/ack ESP32 → Broker {"status": "success", "original_command_id": "cmd123"} Optional: ESP32 publishes to confirm command receipt/execution.
Subscribe to All Commands smarthome/devices/esp32_livingroom_01/commands/# ESP32 Subscription N/A (Subscription Filter) ESP32 uses this wildcard to receive any command under its /commands/ path.

Code Snippets (ESP-IDF v5.x)

We’ll focus on the parts of the code that construct and use these topics. Assume client is your esp_mqtt_client_handle_t.

C
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "mqtt_client.h"
#include "cJSON.h" // For creating JSON payloads

static const char *TAG = "MQTT_TOPIC_EXAMPLE";

// Assume client is initialized and connected elsewhere
extern esp_mqtt_client_handle_t client; 

// Device specific information
#define DEVICE_ID "esp32_livingroom_01"
#define BASE_TOPIC_PREFIX "smarthome/devices/"

// Buffer for constructing topic strings
#define MAX_TOPIC_LEN 128
char topic_buffer[MAX_TOPIC_LEN];

// --- Publishing Telemetry ---
void publish_temperature(float temp_c) {
    snprintf(topic_buffer, MAX_TOPIC_LEN, "%s%s/telemetry/temperature", BASE_TOPIC_PREFIX, DEVICE_ID);

    cJSON *root = cJSON_CreateObject();
    if (root == NULL) {
        ESP_LOGE(TAG, "Failed to create JSON object for temperature");
        return;
    }
    cJSON_AddNumberToObject(root, "value", temp_c);
    cJSON_AddStringToObject(root, "unit", "C");

    char *payload_str = cJSON_PrintUnformatted(root);
    if (payload_str == NULL) {
        ESP_LOGE(TAG, "Failed to print JSON to string for temperature");
        cJSON_Delete(root);
        return;
    }

    int msg_id = esp_mqtt_client_publish(client, topic_buffer, payload_str, 0, 1, 0); // QoS 1, retain 0
    if (msg_id != -1) {
        ESP_LOGI(TAG, "Sent publish successful, topic=%s, msg_id=%d", topic_buffer, msg_id);
        ESP_LOGI(TAG, "Payload: %s", payload_str);
    } else {
        ESP_LOGE(TAG, "Sent publish failed, topic=%s", topic_buffer);
    }
    
    cJSON_Delete(root);
    free(payload_str);
}

void publish_humidity(float humidity_percent) {
    snprintf(topic_buffer, MAX_TOPIC_LEN, "%s%s/telemetry/humidity", BASE_TOPIC_PREFIX, DEVICE_ID);
    
    cJSON *root = cJSON_CreateObject();
    // ... (similar JSON creation as for temperature) ...
    cJSON_AddNumberToObject(root, "value", humidity_percent);
    cJSON_AddStringToObject(root, "unit", "%");
    char *payload_str = cJSON_PrintUnformatted(root);
    // ... (similar publish logic and cleanup) ...
    
    int msg_id = esp_mqtt_client_publish(client, topic_buffer, payload_str, 0, 1, 0);
     if (msg_id != -1) {
        ESP_LOGI(TAG, "Sent publish successful, topic=%s, msg_id=%d", topic_buffer, msg_id);
        ESP_LOGI(TAG, "Payload: %s", payload_str);
    } else {
        ESP_LOGE(TAG, "Sent publish failed, topic=%s", topic_buffer);
    }

    cJSON_Delete(root);
    free(payload_str);
}


// --- Handling Incoming Commands ---
// This function would be called from your MQTT event handler when MQTT_EVENT_DATA is received
void handle_incoming_command(const char *topic, int topic_len, const char *data, int data_len) {
    char received_topic[MAX_TOPIC_LEN];
    // Ensure null termination for topic string
    int len_to_copy = topic_len < MAX_TOPIC_LEN -1 ? topic_len : MAX_TOPIC_LEN -1;
    strncpy(received_topic, topic, len_to_copy);
    received_topic[len_to_copy] = '\0';

    ESP_LOGI(TAG, "Command received on topic: %s", received_topic);
    ESP_LOGI(TAG, "Data: %.*s", data_len, data);

    // Construct the expected command topic for set_led
    char expected_set_led_topic[MAX_TOPIC_LEN];
    snprintf(expected_set_led_topic, MAX_TOPIC_LEN, "%s%s/commands/set_led", BASE_TOPIC_PREFIX, DEVICE_ID);

    if (strncmp(received_topic, expected_set_led_topic, topic_len) == 0) {
        cJSON *root = cJSON_ParseWithLength(data, data_len);
        if (root == NULL) {
            ESP_LOGE(TAG, "Failed to parse command JSON");
            return;
        }
        cJSON *state_item = cJSON_GetObjectItemCaseSensitive(root, "state");
        if (cJSON_IsString(state_item) && (state_item->valuestring != NULL)) {
            if (strcmp(state_item->valuestring, "ON") == 0) {
                ESP_LOGI(TAG, "Turning LED ON");
                // Add your GPIO logic here to turn LED ON
            } else if (strcmp(state_item->valuestring, "OFF") == 0) {
                ESP_LOGI(TAG, "Turning LED OFF");
                // Add your GPIO logic here to turn LED OFF
            } else {
                ESP_LOGW(TAG, "Unknown LED state: %s", state_item->valuestring);
            }
            // Optionally publish an acknowledgement
            char ack_topic[MAX_TOPIC_LEN];
            snprintf(ack_topic, MAX_TOPIC_LEN, "%s/ack", expected_set_led_topic);
            esp_mqtt_client_publish(client, ack_topic, "{\"status\":\"success\"}", 0, 0, 0);

        } else {
            ESP_LOGE(TAG, "Invalid or missing 'state' in set_led command payload");
        }
        cJSON_Delete(root);
    } else {
        ESP_LOGW(TAG, "Received command on unhandled topic: %s", received_topic);
    }
}

// --- Subscribing to Command Topics ---
// This function would be called after the MQTT client connects
void subscribe_to_commands(void) {
    // Subscribe to all commands for this device
    snprintf(topic_buffer, MAX_TOPIC_LEN, "%s%s/commands/#", BASE_TOPIC_PREFIX, DEVICE_ID);
    
    int msg_id = esp_mqtt_client_subscribe(client, topic_buffer, 1); // QoS 1
    if (msg_id != -1) {
        ESP_LOGI(TAG, "Sent subscribe successful, topic=%s, msg_id=%d", topic_buffer, msg_id);
    } else {
        ESP_LOGE(TAG, "Sent subscribe failed, topic=%s", topic_buffer);
    }

    // Example of subscribing to a specific command if needed, though the wildcard above covers it
    // snprintf(topic_buffer, MAX_TOPIC_LEN, "%s%s/commands/set_led", BASE_TOPIC_PREFIX, DEVICE_ID);
    // esp_mqtt_client_subscribe(client, topic_buffer, 1);
}

/*
In your main MQTT event handler (esp_mqtt_event_handle_cb_t):

static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
    esp_mqtt_event_handle_t event = event_data;
    esp_mqtt_client_handle_t client = event->client; // Or use the global client handle
    
    switch ((esp_mqtt_event_id_t)event_id) {
        case MQTT_EVENT_CONNECTED:
            ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
            // Subscribe to command topics once connected
            subscribe_to_commands(); 
            // Example: publish initial status or a test message
            publish_temperature(25.0); 
            publish_humidity(50.0);
            break;
        case MQTT_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
            break;
        case MQTT_EVENT_SUBSCRIBED:
            ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
            break;
        case MQTT_EVENT_UNSUBSCRIBED:
            ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
            break;
        case MQTT_EVENT_PUBLISHED:
            ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
            break;
        case MQTT_EVENT_DATA:
            ESP_LOGI(TAG, "MQTT_EVENT_DATA");
            // Pass to command handler
            handle_incoming_command(event->topic, event->topic_len, event->data, event->data_len);
            break;
        case MQTT_EVENT_ERROR:
            ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
            if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
                // Log transport errors if needed
            }
            break;
        default:
            ESP_LOGI(TAG, "Other event id:%d", event->id);
            break;
    }
}
*/

Tip: Add the cJSON component to your project’s CMakeLists.txt if it’s not already included:

PRIV_REQUIRES cJSON or REQUIRES cJSON in your component CMakeLists.txt.

Or add idf_component_get_property(cjson_lib cJSON CJSON_LIB) and link cjson_lib to your main executable target.

Ensure your sdkconfig has CONFIG_ESP_MQTT_CLIENT_ENABLE_JSON_CONFIG=y if you are configuring the client via JSON (not directly related to topic design but good to be aware of). For payload creation as shown, cJSON is typically added as a separate component.

Build Instructions

  1. Create a new ESP-IDF project or use an existing one.
  2. Add the code:
    • Place the C code snippets into your main application file (e.g., main.c) or organize them into appropriate .c and .h files within your main component.
    • Ensure you have an MQTT client initialization function and the event handler set up as shown in Chapter 102.
    • Make sure cJSON is correctly linked. In ESP-IDF v5.x, it’s usually available as a component. Add idf_component_manager_add_dependency("cJSON" "YOUR_cJSON_VERSION_OR_LATEST") to your project CMakeLists.txt or use idf.py add-dependency "espressif/cjson^1.7.15" (check for latest compatible version). Then, in your component’s CMakeLists.txt: REQUIRES cJSON.
  3. Configure:
    • Use idf.py menuconfig to set up your WiFi credentials.
    • Under Component config --> ESP MQTT Client, configure your MQTT broker URI (e.g., mqtt://your_broker_ip or mqtts://your_secure_broker_domain).
  4. Build:idf.py build

Run/Flash/Observe Steps

  1. Flash the firmware:idf.py -p /dev/ttyUSB0 flash monitor
    (Replace /dev/ttyUSB0 with your ESP32’s serial port).
  2. Observe Device Logs:
    • The ESP32 will connect to WiFi and then to the MQTT broker.
    • It will subscribe to smarthome/devices/esp32_livingroom_01/commands/#.
    • It will publish initial temperature and humidity readings to:
      • smarthome/devices/esp32_livingroom_01/telemetry/temperature
      • smarthome/devices/esp32_livingroom_01/telemetry/humidity
  3. Use an MQTT Client Tool (e.g., MQTTX or mosquitto_sub/mosquitto_pub):
    • Subscribe to Telemetry:
      • Subscribe to smarthome/devices/esp32_livingroom_01/telemetry/# to see both temperature and humidity.
      • Or subscribe specifically: smarthome/devices/esp32_livingroom_01/telemetry/temperature and smarthome/devices/esp32_livingroom_01/telemetry/humidity.
      • You should see the JSON payloads published by the ESP32.
    • Subscribe to All Device Topics (for debugging):
      • Subscribe to smarthome/devices/esp32_livingroom_01/# to see all messages related to this device, including telemetry and command acknowledgements.
    • Publish a Command:
      • Publish a message to: smarthome/devices/esp32_livingroom_01/commands/set_led
      • Payload: {"state": "ON"}
      • Observe the ESP32’s log output. It should print “Turning LED ON”.
      • If you implemented the LED GPIO logic, the LED on your ESP32 development board should turn on.
      • Publish {"state": "OFF"} to turn it off.
      • If you implemented command acknowledgements, you should see a message on smarthome/devices/esp32_livingroom_01/commands/set_led/ack.

Tip: Using snprintf to build topic strings is safer than sprintf as it helps prevent buffer overflows. Always ensure your buffer is large enough for the longest possible topic string.

Variant Notes

The principles of MQTT topic design are universal and not specific to any particular ESP32 variant (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2). All these chips use the same ESP-MQTT client library and interact with MQTT brokers in the same way concerning topic structures.

However, consider these aspects related to variants:

  1. Unique Device ID:
    • All ESP32 variants have a unique MAC address that can be used as a basis for a device ID.#include "esp_mac.h" // ... uint8_t mac[6]; char mac_str[18]; // 12 hex chars + 5 colons + null terminator esp_efuse_mac_get_default(mac); // Or esp_read_mac for specific interface sprintf(mac_str, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); // Use mac_str (or a version without colons) in your topic.
    • Some applications might use provisioned serial numbers or other identifiers stored in NVS.
  2. Resource Constraints (Older/Smaller Variants like ESP32-C3):
    • While topic strings themselves are relatively small, very long topic strings, combined with many subscriptions, can consume RAM. This is generally not a major issue with typical topic lengths, but for highly constrained applications on variants with less RAM (like some ESP32-C3 configurations), being concise can be marginally beneficial.
    • The primary impact is on the number of active subscriptions and the memory used by the MQTT client to store topic filters and incoming message buffers, not the topic design pattern itself.
  3. Functionality Differences:
    • The data being published might differ based on the variant’s capabilities (e.g., an ESP32-S3 with AI acceleration might publish inference results, while an ESP32-C3 might publish simpler sensor data). The topic structure should reflect the type of data or capability.
    • Example:
      • devices/[device_id]/sensors/temperature (common to all)
      • devices/[device_id]/ai/person_detection/count (specific to an ESP32-S3 with a camera and AI model)

The core topic design strategies (hierarchical, device-centric, command/telemetry separation) remain equally applicable and recommended across all ESP32 variants. The choice of variant influences what data you send and what capabilities your device has, which then feeds into the specifics of your topic levels, but not the fundamental patterns of organization.

The ESP32-H2, with its Thread and Zigbee capabilities, might be part of larger ecosystems where MQTT is used at a gateway level. In such cases, the gateway would be responsible for translating between, say, Zigbee device messages and the MQTT topic structure. The ESP32-H2 itself, if acting as an MQTT client (e.g., in a WiFi + Thread/Zigbee co-processor mode or as a Thread Border Router with MQTT capabilities), would still adhere to these topic design principles for its MQTT communications.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Description / Example Impact Solution / Fix
Inconsistent Levels/Naming Using sensors/livingroom/temp and sensors/living_room/temperature. Or mixing device/ID/data with ID/telemetry. Confusion, difficult wildcard subscriptions, harder maintenance and debugging. Establish and document a clear naming convention (e.g., lowercase, snake_case) and hierarchy. Use constants/macros for base topics.
Incorrect Wildcard Usage Subscribing to sensors/#/temperature (invalid, # must be last). Or using + expecting it to match multiple levels. Subscription doesn’t work as intended, messages missed or unexpected messages received. Understand + is for one level, # for multiple levels at the end. Test subscriptions with an MQTT tool.
Overly Broad Subscriptions ESP32 subscribes to # or smarthome/# when it only needs smarthome/devices/DEVICE_ID/commands/#. Wastes device resources (CPU, RAM, bandwidth) processing irrelevant messages. Devices should subscribe only to the most specific topics they need.
Forgetting Device Uniqueness Multiple devices publishing to the exact same topic, e.g., office/temperature. Impossible to distinguish message sources without inspecting payload; complicates routing and ACLs. Always include a unique device identifier as a distinct level in the topic string (e.g., office/DEVICE_ID/temperature).
Not Distinguishing Message Types Using a single topic like devices/my_esp32/data for both telemetry from ESP32 and commands to ESP32. Ambiguity for the device, hard to process incoming messages correctly, risk of command loops. Use distinct sub-paths like /telemetry, /status, /commands, /events within a device’s topic tree.
Leading/Trailing Slashes or Empty Levels Using /sensors/temp/ or sensors//temp. Non-standard, may lead to unexpected behavior or rejection by some brokers. Reduces clarity. Adhere to MQTT spec: no leading/trailing slashes, no empty topic levels. Use sensors/temp.

Exercises

  1. Smart Office Lighting System:
    • Scenario: Design MQTT topics for a smart office with multiple floors, rooms, and light fixtures per room. Each light can be turned ON/OFF and have its brightness set (0-100%). Lights also report their current state.
    • Task: Define the topic structure for:
      • Publishing a light’s current state (ON/OFF, brightness).
      • Sending a command to turn a specific light ON/OFF.
      • Sending a command to set a specific light’s brightness.
      • Sending a command to turn all lights in a specific room OFF.
      • Sending a command to turn all lights on a specific floor to 50% brightness.
    • Hint: Consider location-centric and function-centric patterns. How would you use wildcards for the “all lights in room/floor” commands on the backend/control application side?
  2. Industrial Sensor Network:
    • Scenario: An industrial plant has various types of sensors (temperature, pressure, vibration) attached to different machines in different zones. Each sensor has a unique serial number. The system needs to track sensor data, sensor status (online/offline/error), and allow for remote sensor calibration commands.
    • Task: Design the MQTT topic structure. Pay attention to:
      • Identifying sensors uniquely.
      • Distinguishing between data, status, and commands.
      • Allowing subscriptions to all data from a specific machine or zone.
      • Allowing subscriptions to all temperature readings across the plant.
    • Bonus: How would you incorporate a topic for sensor event alerts (e.g., pressure_too_high)?
  3. Multi-Tenant Pet Tracker:
    • Scenario: You are building an IoT platform for multiple pet tracking companies. Each company (tenant) has multiple customers, and each customer can have one or more pet tracking devices. Devices report GPS location and battery level.
    • Task: Design an MQTT topic structure that:
      • Ensures data isolation between tenants.
      • Allows a specific customer to see data only from their own devices.
      • Allows a tenant company to administer and see data from all their customers’ devices.
      • Specifies topics for GPS location updates and battery level reports.
    • Hint: Think about how project_id_or_tenant_id and user_id can be incorporated.

Summary

  • MQTT topics are hierarchical UTF-8 strings used to route messages.
  • Well-designed topics are crucial for scalability, maintainability, and security.
  • Wildcards (+ for single-level, # for multi-level at the end) allow flexible subscriptions.
  • Common patterns include device-centric, location-centric, function-centric, and standardized command/telemetry/status structures.
  • Key considerations: hierarchy, uniqueness (device ID is vital), scalability, security, readability, and clear separation of data flow (telemetry vs. commands).
  • Best practices: consistency, specificity, planning for wildcards, and thorough documentation.
  • Dynamic topic construction using snprintf is a common and safe practice in C for ESP32 development.
  • Topic design principles are consistent across all ESP32 variants, though the data published may vary based on variant capabilities.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top