前言:HTTP 够用了,为什么还要学 MQTT?

上一篇搞定了 HTTP 请求,ESP32-S3 已经能跟服务器一问一答了。但我很快发现一个问题——HTTP 是"你问我才答"的模式

设备想知道有没有新指令?发个请求问一下。过一秒再问一下。再过一秒再问……这就是所谓的轮询,既浪费流量又浪费电。

而真正的物联网场景需要的是:服务器有新消息,能主动推送给设备。 这正是 MQTT 干的事。

MQTT 是一种基于发布/订阅模式的轻量级协议,专门为物联网场景设计。设备订阅一个主题,有人往这个主题发消息,所有订阅者都能实时收到。就像微信群——你进了群就能收到所有消息,不需要反复去刷。

今天就来记录我从零实现 MQTT 客户端的过程,包括最简单的公共服务器测试,以及连接涂鸦云平台的完整实战。


一、30 秒搞懂 MQTT

核心概念

发布者 (Publisher)              MQTT 服务器 (Broker)              订阅者 (Subscriber)
      |                              |                              |
      |  发布消息到 /topic/temp        |                              |
      | ---------------------------> |                              |
      |                              |  转发给所有订阅了该主题的人       |
      |                              | ---------------------------> |
      |                              |                              | 收到消息!

三个关键角色:

  • Broker(代理/服务器):消息中转站,负责接收和分发消息
  • Publisher(发布者):往某个主题发消息的人
  • Subscriber(订阅者):关注某个主题、等着收消息的人

一个设备可以同时是发布者和订阅者。比如温度传感器既可以发布温度数据,也可以订阅控制指令。

QoS 服务质量

QoS 等级 含义 特点
QoS 0 最多发一次 可能丢消息,但最快
QoS 1 至少发一次 不丢消息,但可能重复
QoS 2 恰好发一次 最可靠,但最慢

HTTP vs MQTT

HTTP MQTT
模式 请求-响应(一问一答) 发布-订阅(实时推送)
连接 短连接,用完就断 长连接,一直保持
方向 客户端主动发起 双向,服务器能主动推
适用场景 偶尔获取数据 实时通信、设备控制
开销 较大(每次带完整头部) 极小(最小 2 字节头)

二、ESP-MQTT 的实现流程

ESP-IDF 内置了 MQTT 客户端库,使用流程非常清晰:

配置服务器参数(URI、凭据、证书)
    ↓
初始化客户端 → esp_mqtt_client_init()
    ↓
注册事件回调 → esp_mqtt_client_register_event()
    ↓
启动客户端 → esp_mqtt_client_start()
    ↓
在事件回调中处理业务(订阅、发布、接收)

跟 HTTP 客户端的套路几乎一样:配置 → 初始化 → 执行 → 回调处理。ESP-IDF 的 API 设计风格真的很统一,学过一个,后面的都容易上手。


三、实战一:连接公共 MQTT 服务器(TCP,无认证)

先从最简单的开始——连接 Eclipse 的公共 MQTT 服务器,不需要任何账号密码。

3.1 MQTT 事件回调

MQTT 的所有状态变化都通过事件回调通知,这是整个 MQTT 逻辑的核心:

// mqtt事件处理
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;
    int msg_id;

    switch ((esp_mqtt_event_id_t)event_id) {
    case MQTT_EVENT_CONNECTED:
        // 连接成功!可以开始订阅和发布了
        ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");

        // 发布一条消息到 /topic/qos1
        msg_id = esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0);
        ESP_LOGI(TAG, "发布成功, msg_id=%d", msg_id);

        // 订阅两个主题
        msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0);
        ESP_LOGI(TAG, "订阅成功, msg_id=%d", msg_id);
        msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1);
        ESP_LOGI(TAG, "订阅成功, msg_id=%d", msg_id);
        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);
        msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0);
        ESP_LOGI(TAG, "发布成功, msg_id=%d", msg_id);
        break;

    case MQTT_EVENT_UNSUBSCRIBED:
        ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
        break;

    case MQTT_EVENT_PUBLISHED:
        // 发布确认(仅 QoS 1 和 QoS 2 会触发)
        ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
        break;

    case MQTT_EVENT_DATA:
        // 收到订阅的消息!这是最重要的事件
        ESP_LOGI(TAG, "MQTT_EVENT_DATA");
        printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
        printf("DATA=%.*s\r\n", event->data_len, event->data);
        break;

    case MQTT_EVENT_ERROR:
        ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
        if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
            ESP_LOGI(TAG, "错误详情: (%s)",
                     strerror(event->error_handle->esp_transport_sock_errno));
        }
        break;

    default:
        ESP_LOGI(TAG, "Other event id:%d", event->event_id);
        break;
    }
}

事件流程一览:

事件 何时触发 通常做什么
MQTT_EVENT_CONNECTED 连接服务器成功 订阅主题、发布初始消息
MQTT_EVENT_DISCONNECTED 连接断开 日志记录,客户端会自动重连
MQTT_EVENT_SUBSCRIBED 订阅被服务器确认 可以开始发布消息
MQTT_EVENT_PUBLISHED 发布被服务器确认 QoS 0 不触发
MQTT_EVENT_DATA 收到订阅的消息 解析并处理消息内容
MQTT_EVENT_ERROR 出错了 排查错误原因

3.2 启动 MQTT 客户端

static void mqtt_app_start(void)
{
    // 配置:只需要一个 URI
    esp_mqtt_client_config_t mqtt_cfg = {
        .broker.address.uri = "mqtt://mqtt.eclipseprojects.io",
    };

    // 初始化
    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);

    // 注册事件回调
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);

    // 启动
    esp_mqtt_client_start(client);
}

就这么几行代码,一个 MQTT 客户端就跑起来了。URI 格式决定了连接方式:

URI 前缀 连接方式 默认端口
mqtt:// TCP 明文 1883
mqtts:// TLS 加密 8883
ws:// WebSocket 80
wss:// WebSocket Secure 443

3.3 整合到 Wi-Fi 配网代码中

MQTT 需要网络连接,所以必须在 Wi-Fi 连接成功后才能启动。我在之前配网代码的基础上,加了一个任务来管理 MQTT 的启动时机:

static bool is_connect_wifi = false;

// Wi-Fi 事件中标记连接状态
// IP_EVENT_STA_GOT_IP → is_connect_wifi = true
// WIFI_EVENT_STA_DISCONNECTED → is_connect_wifi = false

static void mqtt_task(void *pvParameters)
{
    while (1) {
        if (is_connect_wifi) {
            mqtt_app_start();
            // MQTT 启动后就不需要再重复启动了
            while (1) {
                vTaskDelay(1000 / portTICK_PERIOD_MS);
            }
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

// 在 app_main 中创建任务
xTaskCreate(&mqtt_task, "mqtt_task", 9192, NULL, 5, NULL);

编译烧录后,在串口可以看到连接成功、订阅成功、收到消息的完整日志。因为我们订阅了自己发布的主题,所以自己发的消息自己也能收到——这正好验证了发布/订阅机制是正常工作的。
哦豁,连不上,是由于我们当前环境原因,绝对不是代码问题:
在这里插入图片描述


四、实战二:连接涂鸦云平台(TLS 加密)

公共服务器玩玩可以,但实际产品肯定要连自己的云平台。这里我用涂鸦 IoT 平台来演示,因为它免费、有完善的文档,而且支持 MQTT 接入。

4.1 在涂鸦平台创建产品

参考涂鸦官方文档,创建产品后会得到一组参数:
或者根据步骤快速创建;

  1. 创建产品
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 添加自定义功能
    在这里插入图片描述
    在这里插入图片描述
  3. 设备开发
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
ProductID:xxxxxx
DeviceID:xxxxxxxxxx
DeviceSecret:xxxxxxxxx
--------------------
Client ID:xxxxxxxxxxxx
服务器地址:m1.tuyacn.com
端口:8883
用户名:xxxxxxxxxxxxxxxxxx
密码:xxxxxxxxxxxx

4.2 下载并转换证书

涂鸦用的是 TLS 加密连接(端口 8883),所以需要服务器的 CA 根证书。

第一步:涂鸦文档下载根证书。
在这里插入图片描述

第二步: 下载的证书是 DER 格式(二进制),需要转成 PEM 格式(文本):
重命名下载的cer,并利用命令

openssl x509 -inform der -in 1.cer -out tuya_ca.pem

在这里插入图片描述

各参数含义:

  • -inform der:输入格式是 DER(二进制)
  • -in 1.cer:输入文件
  • -out tuya_ca.pem:输出为 PEM 格式

第三步:tuya_ca.pem 放到工程的 main 目录下。

第四步: 修改 main/CMakeLists.txt,把证书嵌入固件:

idf_component_register(
    SRCS "main.c"
    INCLUDE_DIRS "."
    EMBED_TXTFILES tuya_ca.pem
)

4.3 代码中引用证书

extern const uint8_t tuya_ca_pem_start[] asm("_binary_tuya_ca_pem_start");
extern const uint8_t tuya_ca_pem_end[]   asm("_binary_tuya_ca_pem_end");

4.4 配置并启动 MQTT 客户端

跟 TCP 版本相比,TLS 版本多了几个配置项:

static void mqtt_app_start(void)
{
    time_t now_sec = time(NULL);

    // --- 1. Client ID ---
    char client_id[128];
    snprintf(client_id, sizeof(client_id), "tuyalink_%s", TUYA_DEVICE_ID);

    // --- 2. Username ---
    char username[256];
    snprintf(username, sizeof(username),
        "%s|signMethod=hmacSha256,timestamp=%ld,secureMode=1,accessType=1",
        TUYA_DEVICE_ID, (long)now_sec);

    // --- 3. Password (HMAC-SHA256) ---
    // 构造待签名数据: "deviceId=xxx,timestamp=xxx,secureMode=1,accessType=1"
    char sign_data[256];
    snprintf(sign_data, sizeof(sign_data),
        "deviceId=%s,timestamp=%ld,secureMode=1,accessType=1",
        TUYA_DEVICE_ID, (long)now_sec);

    // 计算 HMAC-SHA256
    unsigned char hmac_result[32];  // SHA256 输出 32 字节
    const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
    mbedtls_md_hmac(md_info,
        (const unsigned char *)TUYA_DEVICE_SECRET, strlen(TUYA_DEVICE_SECRET),
        (const unsigned char *)sign_data, strlen(sign_data),
        hmac_result);

    // 转为 64 字符的十六进制字符串 (hexdigest)
    char password[65];
    for (int i = 0; i < 32; i++) {
        sprintf(&password[i * 2], "%02x", hmac_result[i]);
    }
    password[64] = '\0';

    ESP_LOGI(TAG, "Client ID: %s", client_id);
    ESP_LOGI(TAG, "Username:  %s", username);
    ESP_LOGI(TAG, "Password:  %s", password);

    // --- 4. MQTT 配置 ---
    esp_mqtt_client_config_t mqtt_cfg = {
        .broker.address.uri = TUYA_BROKER_URI,
        .credentials.client_id = client_id,
        .credentials.username  = username,
        .credentials.authentication.password = password,
        .broker.verification.certificate = (const char *)tuya_ca_pem_start,
    };

    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
    esp_mqtt_client_start(client);
}

TCP vs TLS 配置对比:

配置项 TCP(公共服务器) TLS(涂鸦云)
URI mqtt://xxx:1883 mqtts://xxx:8883
client_id 不需要(自动生成) 需要填写
username 不需要 需要填写
password 不需要 需要填写
certificate 不需要 需要 CA 证书

可以看到,核心 API 完全一样,只是配置参数多了几项而已。

4.5 完整代码

#include <string.h>
#include <time.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "mbedtls/md.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_smartconfig.h"
#include "esp_http_client.h"
#include "esp_timer.h"
#include "mqtt_client.h"

extern const uint8_t tuya_ca_pem_start[] asm("_binary_tuya_ca_pem_start");
extern const uint8_t tuya_ca_pem_end[]   asm("_binary_tuya_ca_pem_end");

static const char *TAG = "http_client";
static EventGroupHandle_t s_wifi_event_group;
static const int CONNECTED_BIT = BIT0;
static const int ESPTOUCH_DONE_BIT = BIT1;
static bool is_connect_wifi = false;

#define MAX_HTTP_OUTPUT_BUFFER 2048
#define MQTT_PRINT_RX_PAYLOAD 0
#define MQTT_RX_SUMMARY_INTERVAL_MS 5000
#define TUYA_DEVICE_ID     "26a9ee1d18958126f77fqo"   //  DeviceID
#define TUYA_DEVICE_SECRET "H7AtZYpmprtalNnA"      //  DeviceSecret
#define TUYA_BROKER_URI    "mqtts://m1.tuyacn.com:8883"

static void smartconfig_example_task(void *parm);

// mqtt事件处理
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;

    switch ((esp_mqtt_event_id_t)event_id) {
    case MQTT_EVENT_CONNECTED:
        // 连接成功!可以开始订阅和发布了
        ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");

        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:
        // 发布确认(仅 QoS 1 和 QoS 2 会触发)
        ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
        break;

    case MQTT_EVENT_DATA: {
        static uint32_t rx_count = 0;
        static int64_t last_log_ms = 0;

        rx_count++;
#if MQTT_PRINT_RX_PAYLOAD
        ESP_LOGI(TAG, "MQTT_EVENT_DATA");
        printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
        printf("DATA=%.*s\r\n", event->data_len, event->data);
#else
        int64_t now_ms = esp_timer_get_time() / 1000;
        if (now_ms - last_log_ms >= MQTT_RX_SUMMARY_INTERVAL_MS) {
            ESP_LOGI(TAG, "MQTT RX in last %d ms: %lu", MQTT_RX_SUMMARY_INTERVAL_MS, (unsigned long)rx_count);
            rx_count = 0;
            last_log_ms = now_ms;
        }
#endif
        break;
    }

    case MQTT_EVENT_ERROR:
        ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
        if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
            ESP_LOGI(TAG, "错误详情: (%s)",
                     strerror(event->error_handle->esp_transport_sock_errno));
        }
        break;

    default:
        ESP_LOGI(TAG, "Other event id:%d", event->event_id);
        break;
    }
}

// mqtt应用启动
static void mqtt_app_start(void)
{
    time_t now_sec = time(NULL);

    // --- 1. Client ID ---
    char client_id[128];
    snprintf(client_id, sizeof(client_id), "tuyalink_%s", TUYA_DEVICE_ID);

    // --- 2. Username ---
    char username[256];
    snprintf(username, sizeof(username),
        "%s|signMethod=hmacSha256,timestamp=%ld,secureMode=1,accessType=1",
        TUYA_DEVICE_ID, (long)now_sec);

    // --- 3. Password (HMAC-SHA256) ---
    // 构造待签名数据: "deviceId=xxx,timestamp=xxx,secureMode=1,accessType=1"
    char sign_data[256];
    snprintf(sign_data, sizeof(sign_data),
        "deviceId=%s,timestamp=%ld,secureMode=1,accessType=1",
        TUYA_DEVICE_ID, (long)now_sec);

    // 计算 HMAC-SHA256
    unsigned char hmac_result[32];  // SHA256 输出 32 字节
    const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
    mbedtls_md_hmac(md_info,
        (const unsigned char *)TUYA_DEVICE_SECRET, strlen(TUYA_DEVICE_SECRET),
        (const unsigned char *)sign_data, strlen(sign_data),
        hmac_result);

    // 转为 64 字符的十六进制字符串 (hexdigest)
    char password[65];
    for (int i = 0; i < 32; i++) {
        sprintf(&password[i * 2], "%02x", hmac_result[i]);
    }
    password[64] = '\0';

    ESP_LOGI(TAG, "Client ID: %s", client_id);
    ESP_LOGI(TAG, "Username:  %s", username);
    ESP_LOGI(TAG, "Password:  %s", password);

    // --- 4. MQTT 配置 ---
    esp_mqtt_client_config_t mqtt_cfg = {
        .broker.address.uri = TUYA_BROKER_URI,
        .credentials.client_id = client_id,
        .credentials.username  = username,
        .credentials.authentication.password = password,
        .broker.verification.certificate = (const char *)tuya_ca_pem_start,
    };

    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
    esp_mqtt_client_start(client);
}


// ==================== Wi-Fi 事件处理 ====================
static void event_handler(void *arg, esp_event_base_t event_base,
                          int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        xTaskCreate(smartconfig_example_task, "smartconfig_task", 4096, NULL, 3, NULL);
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        is_connect_wifi = false;
        esp_wifi_connect();
        xEventGroupClearBits(s_wifi_event_group, CONNECTED_BIT);
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        xEventGroupSetBits(s_wifi_event_group, CONNECTED_BIT);
        is_connect_wifi = true;
    } else if (event_base == SC_EVENT && event_id == SC_EVENT_SCAN_DONE) {
        ESP_LOGI(TAG, "Scan done");
    } else if (event_base == SC_EVENT && event_id == SC_EVENT_FOUND_CHANNEL) {
        ESP_LOGI(TAG, "Found channel");
    } else if (event_base == SC_EVENT && event_id == SC_EVENT_GOT_SSID_PSWD) {
        ESP_LOGI(TAG, "Got SSID and password");
        smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *)event_data;
        wifi_config_t wifi_config;
        uint8_t ssid[33] = {0};
        uint8_t password[65] = {0};

        bzero(&wifi_config, sizeof(wifi_config_t));
        memcpy(wifi_config.sta.ssid, evt->ssid, sizeof(wifi_config.sta.ssid));
        memcpy(wifi_config.sta.password, evt->password, sizeof(wifi_config.sta.password));
        memcpy(ssid, evt->ssid, sizeof(evt->ssid));
        memcpy(password, evt->password, sizeof(evt->password));
        ESP_LOGI(TAG, "SSID:%s", ssid);
        ESP_LOGI(TAG, "PASSWORD:%s", password);

        ESP_ERROR_CHECK(esp_wifi_disconnect());
        ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
        esp_wifi_connect();
    } else if (event_base == SC_EVENT && event_id == SC_EVENT_SEND_ACK_DONE) {
        xEventGroupSetBits(s_wifi_event_group, ESPTOUCH_DONE_BIT);
    }
}

// ==================== SmartConfig 任务 ====================
static void smartconfig_example_task(void *parm)
{
    EventBits_t uxBits;
    wifi_config_t myconfig = {0};
    esp_wifi_get_config(ESP_IF_WIFI_STA, &myconfig);

    if (strlen((char *)myconfig.sta.ssid) > 0) {
        ESP_LOGI(TAG, "Already configured, SSID: %s, connecting...", myconfig.sta.ssid);
        esp_wifi_connect();
    } else {
        ESP_LOGI(TAG, "No config found, starting SmartConfig...");
        ESP_ERROR_CHECK(esp_smartconfig_set_type(SC_TYPE_ESPTOUCH_AIRKISS));
        smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
        ESP_ERROR_CHECK(esp_smartconfig_start(&cfg));
    }

    while (1) {
        uxBits = xEventGroupWaitBits(s_wifi_event_group,
                    CONNECTED_BIT | ESPTOUCH_DONE_BIT, true, false, portMAX_DELAY);
        if (uxBits & CONNECTED_BIT) {
            ESP_LOGI(TAG, "WiFi Connected to ap");
            vTaskDelete(NULL);
        }
        if (uxBits & ESPTOUCH_DONE_BIT) {
            ESP_LOGI(TAG, "SmartConfig over");
            esp_smartconfig_stop();
            vTaskDelete(NULL);
        }
    }
}

static void mqtt_task(void *pvParameters)
{
    while (1) {
        if (is_connect_wifi) {
            mqtt_app_start();
            // MQTT 启动后就不需要再重复启动了
            while (1) {
                vTaskDelay(1000 / portTICK_PERIOD_MS);
            }
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

// ==================== 主函数 ====================
void app_main(void)
{
    // 减少 MQTT 组件内部日志,避免 monitor 刷屏
    esp_log_level_set("MQTT_CLIENT", ESP_LOG_WARN);
    esp_log_level_set("TRANSPORT_BASE", ESP_LOG_WARN);
    esp_log_level_set("OUTBOX", ESP_LOG_WARN);

    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    s_wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    assert(sta_netif);

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    // 创建 MQTT 任务
    xTaskCreate(&mqtt_task, "mqtt_task", 9192, NULL, 5, NULL);
}

看到串口打印 MQTT_EVENT_CONNECTED,就说明成功连上涂鸦云了!在涂鸦的设备管理后台也能看到设备在线状态。
在这里插入图片描述
在这里插入图片描述
当然后续还有无限的事情要做,我就不做了。


五、MQTT 核心 API 速查

API 功能
esp_mqtt_client_init(config) 初始化客户端
esp_mqtt_client_register_event(client, id, handler, arg) 注册事件回调
esp_mqtt_client_start(client) 启动连接
esp_mqtt_client_subscribe(client, topic, qos) 订阅主题
esp_mqtt_client_unsubscribe(client, topic) 取消订阅
esp_mqtt_client_publish(client, topic, data, len, qos, retain) 发布消息
esp_mqtt_client_stop(client) 停止客户端
esp_mqtt_client_destroy(client) 销毁客户端

六、总结

从配网到上云,完整链路已经打通

NVS(存配网信息)
  ↓
SmartConfig(获取 Wi-Fi 密码)
  ↓
Wi-Fi 连接(拿到 IP)
  ↓
HTTP(一问一答式通信)
  ↓
MQTT(实时双向通信)  ← 本篇
  ↓
真正的物联网产品!

两种 MQTT 接入方式对比

TCP 无认证 TLS + 涂鸦云
URI mqtt:// mqtts://
安全性 低(明文) 高(加密)
认证 需要 client_id + username + password
证书 不需要 需要 CA 根证书
用途 开发测试 生产环境
代码差异 配置项少 多几个配置字段,API 完全一样

踩坑备忘

  1. MQTT 必须在 Wi-Fi 连接成功后才能启动,否则直接报错
  2. 证书格式要注意:涂鸦下载的是 DER 格式,需要用 openssl 转成 PEM
  3. mqtt_app_start 只能调用一次,重复调用会创建多个客户端实例
  4. ESP-MQTT 有自动重连机制,断网后会自动尝试重连,不需要手动处理
  5. 任务栈大小要够:TLS 握手比较吃内存,任务栈建议给 8KB 以上

感悟

学到这里,我突然意识到 ESP-IDF 的 API 设计有一个很明显的规律:所有网络通信类的功能,都是"配置结构体 + 初始化 + 注册事件 + 启动"这套组合拳。 HTTP 是这样,MQTT 也是这样。理解了这个模式,后面再学 WebSocket、CoAP 之类的协议,应该都能很快上手。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐