一条MQTT消息之前,先分清三段链路
这一篇把S01指定的SHT40模块接到同一S3,再把新测量发给你自己管理的局域网broker;独立的命令topic只控制GPIO4普通LED。传感器读数、MQTT传输和实体动作各有验收,互不冒充成功。
固定ESP-IDF5.5.5。协议与CRC使用Sensirion官方C驱动1.1.0原样文件,新增HAL只负责IDF新master I2C的总线和读写,不把Arduino的C++驱动直接混进来。构建时不拉取浮动main,原始BSD许可与新代码MIT归属均在包内。
- 做到这里,你应该看到
- 能从模块端点、C驱动、节点、broker到控制灯分别指出边界。
参考:[1] ESP-IDF v5.5.5 MQTT TCP上游完整应用[2] ESP-IDF v5.5.5 ESP-MQTT说明:事件、重连与遗嘱[3] ESP-IDF v5.5.5 新I2C master bus/device接口[4] Sensirion SHT4x C驱动1.1.0:命令、等待与单位[5] Sensirion C驱动1.1.0 公共I2C/CRC实现[6] SHT4x数据手册V6.4:FD命令、等待、CRC与换算
先分别验收S01模块和P01灯,再组合
固定官方v1.1 N8R8板与Adafruit PID4885;断电接3V3/J1-1到VIN,GND/J1-22到GND,GPIO8/J1-12到SDA,GPIO9/J1-15到SCL,模块3Vo留空。模块已有10k上拉,单模块短线100kHz,不额外堆上拉。裸SHT40或另一料号不能套这个针脚表。
GPIO4/J1-4经330Ω接WP7113ID阳极,阴极回地;GPIO38板载RGB不参与。只用USB-to-UART一条线供电与串口。先断电完成接线;若要做传感器失败实验,预装SDA开关,避免带电挪动电源线。
| 开发板端点 | 器件端点 |
|---|---|
| 3V3 / J1-1 | PID4885 VIN |
| GND / J1-22 | PID4885 GND、LED阴极 |
| GPIO8 / J1-12 | PID4885 SDA |
| GPIO9 / J1-15 | PID4885 SCL |
| GPIO4 / J1-4 | 330Ω → WP7113ID阳极 |
| PID4885 3Vo | 不接 |
- 做到这里,你应该看到
- S01和P01各自能独立验收;针脚、板修订与模块料号都有记录。
参考:[3] ESP-IDF v5.5.5 新I2C master bus/device接口[6] SHT4x数据手册V6.4:FD命令、等待、CRC与换算[7] Adafruit PID4885针脚:VIN/3Vo、0x44、10k上拉[9] 乐鑫 DevKitC-1 v1.1 固定文档:修订、供电与排针[11] Kingbright WP7113ID数据手册
先在本机检查broker,再开放给实验LAN
本例锁定Mosquitto2.0.20及同版pub/sub工具。先按官方包或源码安装到自己的工具目录,下载包的broker-loopback.conf仅监听127.0.0.1:18884,匿名、无持久化,适合先验证主机协议;没有把系统服务作为隐含前置。
设备连接前,在配置副本中把listener改成电脑实际私有LAN IP,并在menuconfig填同一个mqtt://私有IP:18884。localhost在S3上指自己,默认192.0.2.10是占位。只向可信实验LAN开放,不做公网映射;这份匿名明文演示不提供生产认证与TLS。
mosquitto -c broker-loopback.conf -v
# 另开终端,保持订阅器运行
mosquitto_sub -h 127.0.0.1 -p 18884 -t 'hixinwu/n04/#' -v
# 再一个终端发布明确的主机测试消息
mosquitto_pub -h 127.0.0.1 -p 18884 -t hixinwu/n04/test -m host-only- 做到这里,你应该看到
- 先由主机收发host-only,再配置LAN监听;没有把这条消息写成ESP32测量。
参考:[1] ESP-IDF v5.5.5 MQTT TCP上游完整应用[2] ESP-IDF v5.5.5 ESP-MQTT说明:事件、重连与遗嘱[8] Mosquitto 2.0.20正式源码与修订[10] Mosquitto官方配置:listener与allow_anonymous
用新I2C适配层,保持协议驱动原样
通过E01进入5.5.5环境,在项目根目录set-target、menuconfig、build。Wi-Fi凭据只进被忽略的sdkconfig,占位值不联网;同一net.c用事件和退避管理ONLINE。dependencies.lock固定SDK,main/sensirion保留固定上游C文件与LICENSE,THIRD_PARTY.md列明归属。
HAL建立地址0x44、100kHz的新master device,收发各有100ms上限并传播错误。官方高精度命令等待10ms;任务延时按上取整再多一tick,抵消调用时刻的tick相位,避免被提前结束。C驱动的结果是milli°C和milli%RH,使用整数近似换算,再除1000显示,三位小数不代表测量准确度;CRC仍由原样sensirion_i2c.c检查。
idf.py --version
idf.py set-target esp32s3
idf.py menuconfig
idf.py build
idf.py -p PORT flash monitor- 做到这里,你应该看到
- lock、驱动提交和HAL对应同一份下载工程;实际构建成功不替代I2C实物检查。
参考:[1] ESP-IDF v5.5.5 MQTT TCP上游完整应用[3] ESP-IDF v5.5.5 新I2C master bus/device接口[4] Sensirion SHT4x C驱动1.1.0:命令、等待与单位[5] Sensirion C驱动1.1.0 公共I2C/CRC实现[6] SHT4x数据手册V6.4:FD命令、等待、CRC与换算
遥测、命令、状态与在线消息分开
telemetry每5秒发布,QoS1且不retain;每轮新变量,成功才有valid:true和单位明确的新数。sensor读取或CRC失败时valid:false、温湿度null,连续三次失败尝试soft reset。成功湿度的展示裁到0…100%RH,另保留raw;不启用加热器,也不把裁边说成更准确。
命令只接受完整的精确ON或OFF,拒绝其他长度、分片或错误值;发送者不用retain。节点忽略重新订阅时带retain标记的历史命令,避免恢复连接后重放旧动作。led/state发布重新读取的GPIO数字电平并retain,仍须观察实体灯;availability在线和遗嘱也不代表温湿度持续新鲜。
| topic后缀(共同前缀hixinwu/n04/) | 含义 |
|---|---|
| telemetry | 新序号、valid与温湿度;QoS1非retain |
| led/set | 控制方发精确ON或OFF,不用-r |
| led/state | 节点GPIO读回报告;QoS1+retain |
| availability | online / 遗嘱offline;QoS1+retain |
mosquitto_pub -h LAN_IP -p 18884 -t hixinwu/n04/led/set -q 1 -m ON
mosquitto_pub -h LAN_IP -p 18884 -t hixinwu/n04/led/set -q 1 -m OFF
mosquitto_pub -h LAN_IP -p 18884 -t hixinwu/n04/led/set -q 1 -m toggle- 做到这里,你应该看到
- 重复ON不翻转;toggle被拒绝;传感器失败不沿用旧正常值。
参考:[1] ESP-IDF v5.5.5 MQTT TCP上游完整应用[2] ESP-IDF v5.5.5 ESP-MQTT说明:事件、重连与遗嘱[4] Sensirion SHT4x C驱动1.1.0:命令、等待与单位[5] Sensirion C驱动1.1.0 公共I2C/CRC实现[6] SHT4x数据手册V6.4:FD命令、等待、CRC与换算
重连重订阅,离线时不宣称已补发
MQTT在Wi-Fi首次ONLINE后启动,SDK自动重连;每次CONNECTED重新订阅命令,发布在线与GPIO状态。broker离线时本例记录LOCAL_ONLY,新序号仍按测量轮次前进,本应用没有保存离线未提交的采样。SDK内部outbox中已提交的QoS1消息仍可能重传,断线检测前也存在已提交的竞态;接收方可看到重复或较旧序号,不能宣称每条恢复消息都是新采样。
publish返回message id只表示提交到客户端流程,QoS确认也不是下游界面或实体动作完成。停掉并恢复broker,要观察新的CONNECTED、订阅和后续新sequence;不能从旧retain状态推断恢复期间的测量都已送到。默认client_id只供一台节点,两台共用会互相踢线,扩展前先分别设计身份和topic。
- 做到这里,你应该看到
- 能分别说明Wi-Fi状态、MQTT状态、样本valid、序号与实体反馈;离线未提交采样没有被写成保证送达,SDK重传与新采样分别识别。
参考:[1] ESP-IDF v5.5.5 MQTT TCP上游完整应用[2] ESP-IDF v5.5.5 ESP-MQTT说明:事件、重连与遗嘱[8] Mosquitto 2.0.20正式源码与修订[10] Mosquitto官方配置:listener与allow_anonymous
完整节点主程序与HAL归属
下方是工程完整main.c,网络封装与I2C HAL在下载包中;官方协议C文件保持原字节,方便按固定提交核对。GPIO4配置为INPUT_OUTPUT,使电平读回可用;它没有光学传感器,日志会明确说明。
先修改自己的menuconfig,不把SSID、密码或broker私密信息贴进代码和公开日志。项目只控制普通LED,不把这个命令边界泛化为其他执行器。
// SPDX-License-Identifier: MIT
#include <string.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "mqtt_client.h"
#include "sdkconfig.h"
#include "net.h"
#include "sensirion_hal_idf.h"
#include "sensirion_i2c_hal.h"
#include "sht4x_i2c.h"
static const char *TAG = "N04";
static const char *COMMAND_TOPIC = "hixinwu/n04/led/set";
static const char *STATE_TOPIC = "hixinwu/n04/led/state";
static const char *AVAILABILITY_TOPIC = "hixinwu/n04/availability";
static const char *TELEMETRY_TOPIC = "hixinwu/n04/telemetry";
static EventGroupHandle_t mqtt_flags;
static void publish_led(esp_mqtt_client_handle_t client) {
esp_mqtt_client_publish(client, STATE_TOPIC, gpio_get_level(GPIO_NUM_4) ? "ON" : "OFF", 0, 1, 1);
}
static void mqtt_event(void *arg, esp_event_base_t base, int32_t id, void *data) {
(void)arg; (void)base;
esp_mqtt_event_handle_t event = data;
if (id == MQTT_EVENT_CONNECTED) {
xEventGroupSetBits(mqtt_flags, BIT0);
ESP_LOGI(TAG, "MQTT_CONNECTED: subscribing to command on each connection");
esp_mqtt_client_subscribe(event->client, COMMAND_TOPIC, 1);
esp_mqtt_client_publish(event->client, AVAILABILITY_TOPIC, "online", 0, 1, 1);
publish_led(event->client);
} else if (id == MQTT_EVENT_DISCONNECTED) {
xEventGroupClearBits(mqtt_flags, BIT0); ESP_LOGW(TAG, "MQTT_DISCONNECTED; SDK reconnect enabled");
} else if (id == MQTT_EVENT_DATA) {
const bool topic_ok = event->topic && event->topic_len == strlen(COMMAND_TOPIC) && !memcmp(event->topic, COMMAND_TOPIC, event->topic_len);
// Bounded whole-message commands only. Never apply the first fragment of a longer message.
const bool whole = event->current_data_offset == 0 && event->data_len == event->total_data_len && event->data_len <= 3;
const bool turn_on = whole && event->data_len == 2 && !memcmp(event->data, "ON", 2);
const bool turn_off = whole && event->data_len == 3 && !memcmp(event->data, "OFF", 3);
if (topic_ok && !event->retain && (turn_on || turn_off)) {
gpio_set_level(GPIO_NUM_4, turn_on); publish_led(event->client);
ESP_LOGI(TAG, "LED gpio-digital-level=%s (not an optical measurement)", turn_on ? "ON" : "OFF");
} else ESP_LOGW(TAG, "COMMAND_REJECTED: exact non-retained ON/OFF whole payload required");
} else if (id == MQTT_EVENT_ERROR) ESP_LOGW(TAG, "MQTT_ERROR; inspect local broker and network, credentials are not logged");
}
void app_main(void) {
gpio_reset_pin(GPIO_NUM_4); gpio_set_direction(GPIO_NUM_4, GPIO_MODE_INPUT_OUTPUT); gpio_set_level(GPIO_NUM_4, 0);
mqtt_flags = xEventGroupCreate(); if (!mqtt_flags) { ESP_LOGE(TAG, "allocation failed"); return; }
net_start(); sensirion_i2c_hal_init(); sht4x_init(0x44);
ESP_LOGI(TAG, "IDF=%s SHT4x C driver=1.1.0 PID4885 SDA8/SCL9 100kHz; hardware unverified", esp_get_idf_version());
const esp_mqtt_client_config_t config = {.broker.address.uri = CONFIG_DEMO_BROKER_URL, .credentials.client_id = "hixinwu-n04-demo", .session.last_will = {.topic = AVAILABILITY_TOPIC, .msg = "offline", .qos = 1, .retain = 1}, .network = {.timeout_ms = 3000, .reconnect_timeout_ms = 3000}};
esp_mqtt_client_handle_t client = esp_mqtt_client_init(&config);
if (!client) { ESP_LOGE(TAG, "MQTT init failed"); return; }
ESP_ERROR_CHECK(esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event, NULL));
bool started = false; uint32_t sequence = 0, failures = 0;
for (;;) {
if (!started && net_online()) { ESP_ERROR_CHECK(esp_mqtt_client_start(client)); started = true; }
int32_t temp_milli = 0, humidity_milli = 0;
int16_t result = demo_i2c_ready() ? sht4x_measure_high_precision(&temp_milli, &humidity_milli) : -1;
char payload[256]; ++sequence;
if (result == 0) {
failures = 0;
int32_t displayed_rh = humidity_milli < 0 ? 0 : humidity_milli > 100000 ? 100000 : humidity_milli;
snprintf(payload, sizeof(payload), "{\"sequence\":%" PRIu32 ",\"uptime_ms\":%" PRIi64 ",\"valid\":true,\"temperature_c\":%.3f,\"humidity_raw_percent\":%.3f,\"humidity_percent\":%.3f}", sequence, esp_timer_get_time()/1000, temp_milli/1000.0, humidity_milli/1000.0, displayed_rh/1000.0);
} else {
++failures;
snprintf(payload, sizeof(payload), "{\"sequence\":%" PRIu32 ",\"valid\":false,\"temperature_c\":null,\"humidity_percent\":null,\"sensor_error\":%d}", sequence, result);
ESP_LOGW(TAG, "SENSOR_ERROR=%d HAL=%s; current values invalid", result, esp_err_to_name(demo_i2c_last_error()));
if (failures % 3 == 0 && demo_i2c_ready()) ESP_LOGW(TAG, "soft_reset result=%d", sht4x_soft_reset());
}
if (started && (xEventGroupGetBits(mqtt_flags) & BIT0)) {
int message = esp_mqtt_client_publish(client, TELEMETRY_TOPIC, payload, 0, 1, 0);
ESP_LOGI(TAG, "publish queued_id=%d valid=%u; queued/ack is not physical delivery proof", message, result == 0);
} else ESP_LOGW(TAG, "LOCAL_ONLY sequence=%" PRIu32 " mqtt=offline; no app sample buffer; submitted SDK QoS1 may retry", sequence);
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
参考:[1] ESP-IDF v5.5.5 MQTT TCP上游完整应用[2] ESP-IDF v5.5.5 ESP-MQTT说明:事件、重连与遗嘱[3] ESP-IDF v5.5.5 新I2C master bus/device接口[4] Sensirion SHT4x C驱动1.1.0:命令、等待与单位[5] Sensirion C驱动1.1.0 公共I2C/CRC实现
把失败注入、恢复和证据留下来
订阅端先看到这台板的新sequence与有效读数,再分别发送ON/OFF、toggle与超长命令,对照GPIO报告和实体LED。停/启broker验证重连重订阅;使用事先安装的SDA开关观察valid:false与null,再恢复并确认后续新测量。每项填写observations.csv,保留真实错误与条件,不用预期文字代填。
ONLINE却连不上broker,先用同网主机pub/sub检查IP、监听地址和端口;MQTT正常而sensor_error,回到S01的0x44、供电、8/9与CRC。本站已经实际编译,并在主机运行Mosquitto2.0.20验证QoS1、晚订阅读取retain与清除;没有本站ESP32联网、实体动作、实物CRC注入或broker恢复证据,四维状态如实区分。结束后停临时broker和订阅器,清测试retain。
- 做到这里,你应该看到
- 真实测量、主机合成消息与未执行实板步骤分别可追溯。
参考:[1] ESP-IDF v5.5.5 MQTT TCP上游完整应用[2] ESP-IDF v5.5.5 ESP-MQTT说明:事件、重连与遗嘱[3] ESP-IDF v5.5.5 新I2C master bus/device接口[5] Sensirion C驱动1.1.0 公共I2C/CRC实现[7] Adafruit PID4885针脚:VIN/3Vo、0x44、10k上拉[8] Mosquitto 2.0.20正式源码与修订[10] Mosquitto官方配置:listener与allow_anonymous
资料来源与版本说明
按指定板级修订、框架与上游源码制作。真实构建、主机辅助检查与实板行为分别记录;没有把主机测试或示意图写成本站硬件结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- ESP-IDF v5.5.5 MQTT TCP上游完整应用
- ESP-IDF v5.5.5 ESP-MQTT说明:事件、重连与遗嘱
- ESP-IDF v5.5.5 新I2C master bus/device接口
- Sensirion SHT4x C驱动1.1.0:命令、等待与单位
- Sensirion C驱动1.1.0 公共I2C/CRC实现
- SHT4x数据手册V6.4:FD命令、等待、CRC与换算
- Adafruit PID4885针脚:VIN/3Vo、0x44、10k上拉
- Mosquitto 2.0.20正式源码与修订
- 乐鑫 DevKitC-1 v1.1 固定文档:修订、供电与排针
- Mosquitto官方配置:listener与allow_anonymous
- Kingbright WP7113ID数据手册