【ESP32笔记】ESP32连接MQTT broker 本节目标WiFi 连上路由器后用 ESP-IDF 的 esp-mqtt 组件连接公共 Broker实现自动订阅 每 2 秒发布一条消息一、本节要解决的问题第 16 节只让 ESP32 连上了 WiFi设备数据还没有去处。要联网传输消息需要一个双方都能访问的中转站——MQTT Broker。自己搭 EMQX、Mosquitto 也行但学习测试阶段用公共 MQTT 服务器最省事教程推荐的是国内的mqtt://broker-cn.emqx.ioMQTT 默认端口 1883国外服务器往往有延时和可访问性问题。本节工程完成的事连 WiFi → 等拿到 IP → 启动 MQTT → 连接成功自动订阅/test/topic2→ 主循环每 2 秒向/test/topic1发布一条 JSON 消息。第 18 节讲的发布/订阅、QoS 概念在这里全部落地成代码。二、相关知识测试环境准备项目值Broker 地址mqtt://broker-cn.emqx.io端口1883普通 TCPTLS 加密则为 8883ESP32 发布主题/test/topic1ESP32 订阅主题/test/topic2调试准备教程做法在电脑上安装并打开 MQTTX官网mqttx.app新建连接填地址broker-cn.emqx.io、端口 1883其余默认点 Connect然后新建订阅主题/test/topic1、QoS1——这样 ESP32 发布的消息会显示在电脑上双向测试时在 MQTTX 里向/test/topic2发布消息串口会打印出来。三、知识点3.1 为什么必须先连 WiFi 再连 MQTTMQTT 建立在 TCP 之上TCP 又需要 IP 地址所以顺序必须是WiFi STA 连上路由器 → 拿到 DHCP 分配的 IPIP_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_clientesp_mqtt_client_init(mqtt_cfg);// 1. 用配置创建客户端返回操作句柄esp_mqtt_client_register_event(...);// 2. 注册事件回调处理连接成功/收到数据等事件esp_mqtt_client_start(s_mqtt_client);// 3. 启动内部任务开始连接 Brokeresp_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收到发布确认 PUBACKQoS0 才有确认消息已被 Broker 接收MQTT_EVENT_DATA收到所订阅主题的消息解析 topic / data回调运行在 esp-mqtt 内部任务的上下文里可以做打印、订阅这类操作但不要在里面长时间阻塞。本工程订阅、发布都用QoS1至少一次兼顾可靠与开销发布 QoS1 时 Broker 会回 PUBACK对应MQTT_EVENT_PUBLISHED事件。四、代码分析4.1 工程结构mqtt/ ├── CMakeLists.txt ├── sdkconfig # 目标芯片 esp32MQTT 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_emain组件的 CMakeLists 没写REQUIRESmain是 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_cbNULL;...caseIP_EVENT_STA_GOT_IP://只有获取到路由器分配的IP才认为是连上了路由器if(wifi_cb)wifi_cb(WIFI_CONNECTED);ESP_LOGI(TAG,get ip address);break;...wifi_cbf;//wifi_sta_init() 里保存传进来的回调main.c里实现这个回调它只做一件事给事件组置位。voidwifi_event_handler(WIFI_EV_e ev){if(evWIFI_CONNECTED){xEventGroupSetBits(s_wifi_ev,WIFI_CONNECT_BIT);}}工程里 WiFi 的 SSID/密码是占位值DEFAULT_WIFI_SSID testwifi、DEFAULT_WIFI_PASSWORD 12345678simple_wifi_sta.c开头。必须改成你家路由器的 2.4G 账号密码否则 WiFi 一直重连MQTT 永远不会启动。4.3 app_main先等 WiFi再启动 MQTTvoidapp_main(void){esp_err_tretnvs_flash_init();if(retESP_ERR_NVS_NO_FREE_PAGES||retESP_ERR_NVS_NEW_VERSION_FOUND){//NVS出现错误执行擦除ESP_ERROR_CHECK(nvs_flash_erase());//重新尝试初始化ESP_ERROR_CHECK(nvs_flash_init());}s_wifi_evxEventGroupCreate();EventBits_t ev0;//初始化WIFI传入回调函数用于通知连接成功事件wifi_sta_init(wifi_event_handler);//一直监听WIFI连接事件直到WiFi连接成功后才启动MQTT连接evxEventGroupWaitBits(s_wifi_ev,WIFI_CONNECT_BIT,pdTRUE,pdFALSE,portMAX_DELAY);if(evWIFI_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_ADDRESSmqtt://broker-cn.emqx.io//MQTT连接地址#defineMQTT_PORT1883//MQTT连接端口号#defineMQTT_CLIENTmqttx_d11213//Client ID设备唯一大家最好自行改一下#defineMQTT_USERNAMEhello1//MQTT用户名#defineMQTT_PASSWORD12345678//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.uriMQTT_ADDRESS;mqtt_cfg.broker.address.portMQTT_PORT;//Client IDmqtt_cfg.credentials.client_idMQTT_CLIENT;//用户名mqtt_cfg.credentials.usernameMQTT_USERNAME;//密码mqtt_cfg.credentials.authentication.passwordMQTT_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_clientesp_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()返回客户端句柄失败返回 NULLstart()之后内部任务开始连接默认断线自动重连。日志把密码明文打印出来了。学习例程无所谓实际产品中应删掉这一行避免密码进日志。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_teventevent_data;esp_mqtt_client_handle_tclientevent-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_connectedtrue;//连接成功后订阅测试主题esp_mqtt_client_subscribe_single(s_mqtt_client,MQTT_SUBSCRIBE_TOPIC,1);break;caseMQTT_EVENT_DISCONNECTED://连接断开ESP_LOGI(TAG,mqtt disconnected);s_is_mqtt_connectedfalse;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)本工程 QoS1返回 msg_idBroker 的 SUBACK 到达后触发MQTT_EVENT_SUBSCRIBEDMQTT_EVENT_DATA里event-data、event-topic不以 ‘\0’ 结尾长度分别在event-data_len、event-topic_len必须用%.*s先给长度再给指针打印直接%s可能越界或打出乱码WiFi 掉线会触发MQTT_EVENT_DISCONNECTEDs_is_mqtt_connected置 false主循环暂停发布组件自动重连重连成功再次 CONNECTED订阅和发布恢复。4.6 主循环每 2 秒发布一条 JSONstaticcharmqtt_pub_buff[64];while(1){intcount0;//延时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这里 QoS1、retain0返回 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.hesp_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按顺序应该看到串口打印wifi: connected to AP随后wifi: get ip address——WiFi 拿到 IP打印mqtt connect-clientId:mqttx_d11213,...随后mqtt connected、mqtt subscribed ack, msg_id...——连接并订阅成功MQTTX 里已订阅/test/topic1每约 2 秒收到一条{count:0}串口不断打印mqtt publish ack, msg_id...QoS1 的 PUBACK再在 MQTTX 里向/test/topic2发一条消息串口打印TOPIC/test/topic2和DATAhello。