☰
【ESP32笔记】ESP32连接MQTT broker
2026/10/15 9:27:03 网站建设 项目流程

本节目标:WiFi 连上路由器后,用 ESP-IDF 的 esp-mqtt 组件连接公共 Broker,实现"自动订阅 + 每 2 秒发布一条消息"

一、本节要解决的问题

第 16 节只让 ESP32 连上了 WiFi,设备数据还没有去处。要联网传输消息,需要一个双方都能访问的"中转站"——MQTT Broker。自己搭 EMQX、Mosquitto 也行,但学习测试阶段用公共 MQTT 服务器最省事;教程推荐的是国内的mqtt://broker-cn.emqx.io(MQTT 默认端口 1883,国外服务器往往有延时和可访问性问题)。

本节工程完成的事:连 WiFi → 等拿到 IP → 启动 MQTT → 连接成功自动订阅/test/topic2→ 主循环每 2 秒向/test/topic1发布一条 JSON 消息。第 18 节讲的发布/订阅、QoS 概念,在这里全部落地成代码。

二、相关知识(测试环境准备)

项目值
Broker 地址mqtt://broker-cn.emqx.io
端口1883(普通 TCP;TLS 加密则为 8883)
ESP32 发布主题/test/topic1
ESP32 订阅主题/test/topic2

调试准备(教程做法):在电脑上安装并打开 MQTTX(官网mqttx.app),新建连接填地址broker-cn.emqx.io、端口 1883,其余默认,点 Connect;然后新建订阅,主题/test/topic1、QoS=1——这样 ESP32 发布的消息会显示在电脑上;双向测试时在 MQTTX 里向/test/topic2发布消息,串口会打印出来。

三、知识点

3.1 为什么必须先连 WiFi 再连 MQTT

MQTT 建立在 TCP 之上,TCP 又需要 IP 地址,所以顺序必须是:WiFi STA 连上路由器 → 拿到 DHCP 分配的 IP(IP_EVENT_STA_GOT_IP)→ 再启动 MQTT。

注意:WIFI_EVENT_STA_CONNECTED只表示物理层连上了路由器,那时还没有 IP;只有IP_EVENT_STA_GOT_IP才算"真正可用"。教程在 WiFi 初始化里加的回调,就是专门通知外部"IP 拿到手了"。

3.2 esp-mqtt 客户端三步走

ESP-IDF 自带 esp-mqtt 组件,把协议细节(CONNECT/CONNACK、心跳、断线重连、QoS 重传)都封装好了,应用层只要三步:

s_mqtt_client=esp_mqtt_client_init(&mqtt_cfg);// 1. 用配置创建客户端,返回操作句柄esp_mqtt_client_register_event(...);// 2. 注册事件回调,处理"连接成功/收到数据"等事件esp_mqtt_client_start(s_mqtt_client);// 3. 启动:内部任务开始连接 Broker

esp_mqtt_client_start()是非阻塞的:它只启动 esp-mqtt 的内部任务,连接结果通过事件回调通知。所以"连接成功后再订阅"必须写在MQTT_EVENT_CONNECTED分支里,写在start()后面会失败——那行代码执行时多半还没连上。

3.3 事件驱动的回调模型

和 WiFi 一样,esp-mqtt 接入了 ESP-IDF 的默认事件循环:程序不轮询"连上没有",而是被动等回调。

事件触发时机通常做什么
MQTT_EVENT_CONNECTED连接 Broker 成功订阅主题、置连接标志
MQTT_EVENT_DISCONNECTED连接断开清连接标志(组件默认自动重连)
MQTT_EVENT_SUBSCRIBED收到订阅确认 SUBACK打印 msg_id,确认订阅生效
MQTT_EVENT_PUBLISHED收到发布确认 PUBACK(QoS>0 才有)确认消息已被 Broker 接收
MQTT_EVENT_DATA收到所订阅主题的消息解析 topic / data

回调运行在 esp-mqtt 内部任务的上下文里,可以做打印、订阅这类操作,但不要在里面长时间阻塞。本工程订阅、发布都用QoS1(至少一次),兼顾可靠与开销;发布 QoS1 时 Broker 会回 PUBACK,对应MQTT_EVENT_PUBLISHED事件。

四、代码分析

4.1 工程结构

mqtt/ ├── CMakeLists.txt ├── sdkconfig # 目标芯片 esp32,MQTT 3.1.1 └── main/ ├── CMakeLists.txt # idf_component_register(SRCS "main.c" "simple_wifi_sta.c" INCLUDE_DIRS ".") ├── main.c # app_main + mqtt_start + MQTT 事件回调 + WiFi 通知回调 ├── simple_wifi_sta.c # 第 16 节的 WiFi STA 代码 + 连接成功回调 └── simple_wifi_sta.h # 对外接口:wifi_sta_init(回调) 和 WIFI_EV_e

main组件的 CMakeLists 没写REQUIRES:main是 ESP-IDF 的特殊组件,会自动依赖所有组件(含 esp-mqtt、esp_wifi)。若把这些代码搬到自己创建的组件里,就必须显式写REQUIRES mqtt esp_wifi nvs_flash ...,否则链接报"未定义引用"。

4.2 WiFi 部分的改动:加一个"连接成功"回调

第 16 节的wifi_sta_init()不带参数,MQTT 工程把接口改成"谁调用谁传回调",WiFi 拿到 IP 时回调通知外部——这就是教程说的"增加了一个回调函数通知主函数 WiFi 已连接"。

simple_wifi_sta.h新增事件枚举和回调类型:

typedefenum{WIFI_DISCONNECTED,//wifi断开WIFI_CONNECTED,//wifi已连接}WIFI_EV_e;typedefvoid(*wifi_event_cb)(WIFI_EV_e);//WIFI STA初始化esp_err_twifi_sta_init(wifi_event_cb f);

simple_wifi_sta.c用静态指针保存回调,并在IP_EVENT_STA_GOT_IP时调用它(省略号处为第 16 节已有代码):

//事件通知回调函数staticwifi_event_cb wifi_cb=NULL;...caseIP_EVENT_STA_GOT_IP://只有获取到路由器分配的IP,才认为是连上了路由器if(wifi_cb)wifi_cb(WIFI_CONNECTED);ESP_LOGI(TAG,"get ip address");break;...wifi_cb=f;//wifi_sta_init() 里保存传进来的回调

main.c里实现这个回调,它只做一件事:给事件组置位。

voidwifi_event_handler(WIFI_EV_e ev){if(ev==WIFI_CONNECTED){xEventGroupSetBits(s_wifi_ev,WIFI_CONNECT_BIT);}}

工程里 WiFi 的 SSID/密码是占位值:DEFAULT_WIFI_SSID "testwifi"、DEFAULT_WIFI_PASSWORD "12345678"(simple_wifi_sta.c开头)。必须改成你家路由器的 2.4G 账号密码,否则 WiFi 一直重连,MQTT 永远不会启动。

4.3 app_main:先等 WiFi,再启动 MQTT

voidapp_main(void){esp_err_tret=nvs_flash_init();if(ret==ESP_ERR_NVS_NO_FREE_PAGES||ret==ESP_ERR_NVS_NEW_VERSION_FOUND){//NVS出现错误,执行擦除ESP_ERROR_CHECK(nvs_flash_erase());//重新尝试初始化ESP_ERROR_CHECK(nvs_flash_init());}s_wifi_ev=xEventGroupCreate();EventBits_t ev=0;//初始化WIFI,传入回调函数,用于通知连接成功事件wifi_sta_init(wifi_event_handler);//一直监听WIFI连接事件,直到WiFi连接成功后,才启动MQTT连接ev=xEventGroupWaitBits(s_wifi_ev,WIFI_CONNECT_BIT,pdTRUE,pdFALSE,portMAX_DELAY);if(ev&WIFI_CONNECT_BIT){mqtt_start();}
  • nvs_flash_init():WiFi 驱动要用 NVS 保存校准数据,必须先初始化;失败(无空闲页/版本不符)就先擦除再初始化,这是官方示例的标准写法;
  • xEventGroupWaitBits(组, 位, pdTRUE, pdFALSE, portMAX_DELAY):pdTRUE=等到后自动清除该位,pdFALSE=不要求所有位到齐,portMAX_DELAY=死等不超时;只有真拿到 IP 才调用mqtt_start(),避免没上网就白连 Broker。

4.4 mqtt_start:配置、注册事件、启动

连接参数集中在文件开头的宏里:

#defineMQTT_ADDRESS"mqtt://broker-cn.emqx.io"//MQTT连接地址#defineMQTT_PORT1883//MQTT连接端口号#defineMQTT_CLIENT"mqttx_d11213"//Client ID(设备唯一,大家最好自行改一下)#defineMQTT_USERNAME"hello1"//MQTT用户名#defineMQTT_PASSWORD"12345678"//MQTT密码#defineMQTT_PUBLIC_TOPIC"/test/topic1"//测试用的,推送消息主题#defineMQTT_SUBSCRIBE_TOPIC"/test/topic2"//测试用的,需要订阅的主题
/** 启动mqtt连接 * @param 无 * @return 无 */voidmqtt_start(void){esp_mqtt_client_config_tmqtt_cfg={0};mqtt_cfg.broker.address.uri=MQTT_ADDRESS;mqtt_cfg.broker.address.port=MQTT_PORT;//Client IDmqtt_cfg.credentials.client_id=MQTT_CLIENT;//用户名mqtt_cfg.credentials.username=MQTT_USERNAME;//密码mqtt_cfg.credentials.authentication.password=MQTT_PASSWORD;ESP_LOGI(TAG,"mqtt connect->clientId:%s,username:%s,password:%s",mqtt_cfg.credentials.client_id,mqtt_cfg.credentials.username,mqtt_cfg.credentials.authentication.password);//设置mqtt配置,返回mqtt操作句柄s_mqtt_client=esp_mqtt_client_init(&mqtt_cfg);//注册mqtt事件回调函数esp_mqtt_client_register_event(s_mqtt_client,ESP_EVENT_ANY_ID,aliot_mqtt_event_handler,s_mqtt_client);//启动mqtt连接esp_mqtt_client_start(s_mqtt_client);}
  • 这是ESP-IDF 5.x 的分组结构体:broker.address.*管地址、credentials.*管身份(概念对照见第 18 节 3.10)。4.x 老版本是.uri、.client_id这类扁平字段;
  • ESP_EVENT_ANY_ID表示订阅所有MQTT 事件,具体哪种在回调的event_id里判断;第 4 个参数s_mqtt_client会原样作为event_handler_arg传回回调(回调里没用它,而是直接用全局句柄,两种都行);
  • esp_mqtt_client_init()返回客户端句柄,失败返回 NULL;start()之后内部任务开始连接,默认断线自动重连。

日志把密码明文打印出来了。学习例程无所谓,实际产品中应删掉这一行,避免密码进日志。

4.5 MQTT 事件回调:连接成功后订阅

staticvoidaliot_mqtt_event_handler(void*event_handler_arg,esp_event_base_tevent_base,int32_tevent_id,void*event_data){esp_mqtt_event_handle_tevent=event_data;esp_mqtt_client_handle_tclient=event->client;// your_context_t *context = event->context;switch((esp_mqtt_event_id_t)event_id){caseMQTT_EVENT_CONNECTED://连接成功ESP_LOGI(TAG,"mqtt connected");s_is_mqtt_connected=true;//连接成功后,订阅测试主题esp_mqtt_client_subscribe_single(s_mqtt_client,MQTT_SUBSCRIBE_TOPIC,1);break;caseMQTT_EVENT_DISCONNECTED://连接断开ESP_LOGI(TAG,"mqtt disconnected");s_is_mqtt_connected=false;break;caseMQTT_EVENT_SUBSCRIBED://收到订阅消息ACKESP_LOGI(TAG," mqtt subscribed ack, msg_id=%d",event->msg_id);break;caseMQTT_EVENT_PUBLISHED://收到发布消息ACKESP_LOGI(TAG,"mqtt publish ack, msg_id=%d",event->msg_id);break;caseMQTT_EVENT_DATA:printf("TOPIC=%.*s\r\n",event->topic_len,event->topic);//收到Pub消息直接打印出来printf("DATA=%.*s\r\n",event->data_len,event->data);break;// 其余分支(UNSUBSCRIBED、ERROR、default)从略,完整代码见 main/main.c}}
  • 订阅写在MQTT_EVENT_CONNECTED里:一是保证"连上了才订阅",二是每次重连成功都会自动重新订阅,网络恢复后不用手动补;
  • esp_mqtt_client_subscribe_single(句柄, 主题, QoS):本工程 QoS=1,返回 msg_id;Broker 的 SUBACK 到达后触发MQTT_EVENT_SUBSCRIBED;
  • MQTT_EVENT_DATA里event->data、event->topic不以 ‘\0’ 结尾,长度分别在event->data_len、event->topic_len,必须用%.*s(先给长度再给指针)打印;直接%s可能越界或打出乱码;
  • WiFi 掉线会触发MQTT_EVENT_DISCONNECTED,s_is_mqtt_connected置 false,主循环暂停发布;组件自动重连,重连成功再次 CONNECTED,订阅和发布恢复。

4.6 主循环:每 2 秒发布一条 JSON

staticcharmqtt_pub_buff[64];while(1){intcount=0;//延时2秒发布一条消息到/test/topic1主题if(s_is_mqtt_connected){snprintf(mqtt_pub_buff,64,"{\"count\":\"%d\"}",count);esp_mqtt_client_publish(s_mqtt_client,MQTT_PUBLIC_TOPIC,mqtt_pub_buff,strlen(mqtt_pub_buff),1,0);count++;}vTaskDelay(pdMS_TO_TICKS(2000));}
  • snprintf拼出 JSON{"count":"0"}(注意%d外面带双引号);esp_mqtt_client_publish(句柄, 主题, 数据, 长度, QoS, retain):长度传strlen比传 0 更明确(传 0 时组件自己算 strlen);这里 QoS=1、retain=0;返回 msg_id,稍后能在MQTT_EVENT_PUBLISHED日志里看到;
  • 发布前检查s_is_mqtt_connected,没连上就跳过,避免无效调用。

小 bug(教程和仓库代码一样):int count = 0;写在while(1)里面,每轮都被清零,count++下一轮就丢——实际发出去的 count 永远是 0。想递增就把int count = 0;移到while外面。

五、关键 API 速查

API作用(所在头文件)
esp_mqtt_client_init()按esp_mqtt_client_config_t创建客户端,返回句柄(mqtt_client.h)
esp_mqtt_client_register_event()注册事件回调,ESP_EVENT_ANY_ID表示全部 MQTT 事件
esp_mqtt_client_start()启动客户端(非阻塞),内部任务负责连接、心跳、重连
esp_mqtt_client_stop()/esp_mqtt_client_destroy()停止 / 销毁客户端(工程未用,释放资源时使用)
esp_mqtt_client_subscribe_single()订阅单个主题,参数含 QoS,返回 msg_id(另有_unsubscribe()取消订阅)
esp_mqtt_client_publish()发布消息:主题、数据、长度、QoS、retain

订阅 API 的版本差异:ESP-IDF 5.2 里esp_mqtt_client_subscribe()是按参数类型自动分派的宏,对应esp_mqtt_client_subscribe_single()(单主题)和_multiple()(多主题)。部分 5.2.x 版本该宏对const char *参数有编译问题(后续版本已修复);本工程直接调用_single,避开了这个坑。

六、编译与现象验证

cdesp32-board/mqtt idf.py build idf.py-pCOM<端口>flash monitor

按顺序应该看到:

  1. 串口打印wifi: connected to AP,随后wifi: get ip address——WiFi 拿到 IP;
  2. 打印mqtt connect->clientId:mqttx_d11213,...,随后mqtt connected、mqtt subscribed ack, msg_id=...——连接并订阅成功;
  3. MQTTX 里(已订阅/test/topic1)每约 2 秒收到一条{"count":"0"},串口不断打印mqtt publish ack, msg_id=...(QoS1 的 PUBACK);再在 MQTTX 里向/test/topic2发一条消息,串口打印TOPIC=/test/topic2和DATA=hello。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询