ESP32-S3 真正“上云“:从零实现 MQTT 客户端,连接涂鸦云平台
前言: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 在涂鸦平台创建产品
参考涂鸦官方文档,创建产品后会得到一组参数:
或者根据步骤快速创建;
- 创建产品



- 添加自定义功能


- 设备开发





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 完全一样 |
踩坑备忘
- MQTT 必须在 Wi-Fi 连接成功后才能启动,否则直接报错
- 证书格式要注意:涂鸦下载的是 DER 格式,需要用
openssl转成 PEM mqtt_app_start只能调用一次,重复调用会创建多个客户端实例- ESP-MQTT 有自动重连机制,断网后会自动尝试重连,不需要手动处理
- 任务栈大小要够:TLS 握手比较吃内存,任务栈建议给 8KB 以上
感悟
学到这里,我突然意识到 ESP-IDF 的 API 设计有一个很明显的规律:所有网络通信类的功能,都是"配置结构体 + 初始化 + 注册事件 + 启动"这套组合拳。 HTTP 是这样,MQTT 也是这样。理解了这个模式,后面再学 WebSocket、CoAP 之类的协议,应该都能很快上手。
更多推荐
所有评论(0)