1. ESP32 配网与 MQTT 上报的真实痛点
做 ESP32/ESP8266 物联网项目,WiFi 配网和数据上报这两件事几乎绕不开。配网阶段要处理 SSID/密码存储、SmartConfig 或 AP 配网、断线重连;上报阶段要接 MQTT Broker、维护心跳、处理 QoS 和遗嘱消息。单设备调试时这些都不算难,但设备一多,问题就集中爆发了。
最典型的混乱来自 Key 管理。假设你有 20 台 ESP32 分布在不同的房间或客户现场,每台设备要连 MQTT、要调云端 API、要做 OTA 校验,如果每台设备都单独申请一套 API Key,那么密钥轮换、权限回收、用量统计就会变成一场灾难。更麻烦的是,很多团队会把 Key 硬编码在固件里,一旦某台设备被拆解,整套凭证就泄露了。
我试过用 TaoToken 的统一 Key 方案来收敛这个问题:设备端只持有一个统一 Key,通过同一个 API 通道完成模型调用、设备鉴权和数据上报的凭证校验。这样固件里不需要塞多套密钥,轮换时只需在控制台更新一次,所有设备重新拉取配置即可生效。下面从环境准备开始,把 config.toml 和 settings.json 的骨架、接入配置、串口日志验证和 MQTT 订阅验证完整走一遍。
2. TaoToken 前置准备:统一 Key 与 API 通道
TaoToken 在这里扮演的角色是「统一凭证网关」。你可以把它理解成一个集中式的 Key 管理中心:设备端不直接持有各个云服务的原始密钥,而是持有一个 TaoToken 颁发的统一 Key,所有请求先经过 TaoToken 的 API 通道完成鉴权和路由,再转发到目标服务。这样做的好处是设备端配置极简,密钥生命周期由控制台统一管理。
开始之前需要完成两件事。第一是拿到统一 Key,第二是确认 API 通道地址。统一 Key 在控制台的 API Keys 页面创建,创建时可以限定权限范围(比如只允许 MQTT 上报和模型对话,不允许管理操作),这样即使 Key 泄露,影响面也可控。API 通道的基础地址是https://taotoken.net/api,所有设备请求都走这个入口。
如果你后续要做长期编码或 Agent 类任务,可以了解 Coding Plan;如果只是想先验证模型对话链路是否通,可以直接用模型对话页面测试。但本篇聚焦的是设备侧接入,所以重点放在 Key 和 API 通道的配置上。
注意:统一 Key 不要硬编码在固件源码里提交到 Git。推荐做法是编译时通过环境变量注入,或者首次配网时由配网页面下发并写入 NVS。
3. 可复制配置:config.toml 与 settings.json 骨架
ESP32 项目通常有两类配置文件:一类是构建期的工程配置(config.toml),一类是运行期的设备参数(settings.json)。前者在编译时确定,后者可以在配网后动态写入。下面给出两份可直接复制的骨架。
3.1 config.toml 骨架
# config.toml - 构建期工程配置 [project] name = "esp32-wifi-mqtt" version = "1.0.0" target = "esp32" [wifi] ssid = "YOUR_WIFI_SSID" password = "YOUR_WIFI_PASSWORD" reconnect_interval_ms = 5000 max_reconnect_retry = 10 [taotoken] api_base = "https://taotoken.net/api" api_key = "${TAOTOKEN_API_KEY}" # 从环境变量注入,不写死 timeout_ms = 8000 retry_count = 3 [mqtt] broker = "mqtt.example.com" port = 1883 client_id_prefix = "esp32-" keepalive_s = 60 qos = 1 topic_report = "device/{device_id}/report" topic_cmd = "device/{device_id}/cmd" [log] level = "INFO" uart_baud = 115200这份配置的关键点是api_key用${TAOTOKEN_API_KEY}占位,编译脚本从环境变量读取后替换。这样源码仓库里不会出现真实 Key。
3.2 settings.json 骨架
{ "device_id": "esp32-001", "wifi": { "ssid": "", "password": "" }, "taotoken": { "api_key": "", "api_base": "https://taotoken.net/api" }, "mqtt": { "broker": "mqtt.example.com", "port": 1883, "username": "", "password": "" }, "report_interval_ms": 10000 }settings.json 在首次配网时由配网页面或串口命令写入 NVS,之后设备启动时从 NVS 读取。如果 NVS 为空,则进入配网模式。
3.3 参数对照表
| 参数 | 所在文件 | 作用 | 建议值 |
|---|---|---|---|
| api_base | config.toml / settings.json | TaoToken API 通道入口 | https://taotoken.net/api |
| api_key | settings.json | 统一 Key,设备鉴权用 | 控制台创建,按设备或按批次 |
| reconnect_interval_ms | config.toml | WiFi 断线重连间隔 | 5000 |
| keepalive_s | config.toml | MQTT 心跳间隔 | 60 |
| qos | config.toml | MQTT 服务质量等级 | 1 |
| report_interval_ms | settings.json | 数据上报周期 | 10000 |
4. 接入配置:ESP32 侧代码与 TaoToken 通道对接
配置骨架有了,接下来把 TaoToken 的 API 通道接进 ESP32 的代码里。核心思路是:设备启动后先连 WiFi,连上后从 NVS 读取统一 Key,然后用这个 Key 去请求 TaoToken 的鉴权接口,拿到一个短期有效的会话凭证,再用这个凭证去连 MQTT 和上报数据。
4.1 WiFi 连接与 NVS 读取
#include "nvs_flash.h" #include "esp_wifi.h" #include "esp_event.h" static void wifi_init_sta(const char *ssid, const char *password) { esp_netif_init(); esp_event_loop_create_default(); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&cfg); wifi_config_t wifi_config = {0}; strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1); strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1); wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(WIFI_IF_STA, &wifi_config); esp_wifi_start(); esp_wifi_connect(); }NVS 读取统一 Key 的部分:
static esp_err_t load_taotoken_key(char *out_key, size_t max_len) { nvs_handle_t handle; esp_err_t err = nvs_open("taotoken", NVS_READONLY, &handle); if (err != ESP_OK) return err; size_t len = max_len; err = nvs_get_str(handle, "api_key", out_key, &len); nvs_close(handle); return err; }4.2 用统一 Key 请求 TaoToken 鉴权
设备拿到统一 Key 后,向 TaoToken API 通道发起鉴权请求,换取会话凭证。请求用 HTTPS,注意 ESP32 需要配置证书或使用esp_tls的默认配置。
static esp_err_t taotoken_auth(const char *api_key, char *out_token, size_t max_len) { esp_http_client_config_t config = { .url = "https://taotoken.net/api/auth/device", .method = HTTP_METHOD_POST, .timeout_ms = 8000, }; esp_http_client_handle_t client = esp_http_client_init(&config); char post_data[256]; snprintf(post_data, sizeof(post_data), "{\"api_key\":\"%s\"}", api_key); esp_http_client_set_header(client, "Content-Type", "application/json"); esp_http_client_set_post_field(client, post_data, strlen(post_data)); esp_err_t err = esp_http_client_perform(client); if (err == ESP_OK) { int status = esp_http_client_get_status_code(client); if (status == 200) { // 从响应体解析 token,写入 out_token } } esp_http_client_cleanup(client); return err; }4.3 MQTT 连接与上报
拿到会话凭证后,用它作为 MQTT 的 password(username 用 device_id),连接 Broker 并上报数据。
static void mqtt_app_start(const char *device_id, const char *token) { esp_mqtt_client_config_t mqtt_cfg = { .broker.address.uri = "mqtt://mqtt.example.com:1883", .credentials.username = device_id, .credentials.authentication.password = token, .session.keepalive = 60, }; esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); esp_mqtt_client_start(client); }上报数据的主题按device/{device_id}/report拼接,payload 用 JSON:
char topic[64]; snprintf(topic, sizeof(topic), "device/%s/report", device_id); char payload[128]; snprintf(payload, sizeof(payload), "{\"temp\":%.1f,\"hum\":%.1f,\"ts\":%lld}", temp, hum, esp_timer_get_time() / 1000000); esp_mqtt_client_publish(client, topic, payload, 0, 1, 0);5. 验证请求与成功结果:串口日志 + MQTT 订阅双重确认
配置写完,必须验证链路真的通了。这里用两个动作交叉确认:串口日志看设备侧状态,MQTT 订阅看服务端是否收到数据。
5.1 串口日志验证
烧录固件后打开串口监视器,波特率 115200。正常启动的日志应该依次出现:
I (312) wifi: state: init -> auth (0xb0) I (425) wifi: state: auth -> assoc (0x0) I (612) wifi: state: assoc -> run (0x10) I (658) esp_netif_handlers: sta ip: 192.168.1.105, mask: 255.255.255.0 I (660) TAOTOKEN: auth request sent I (892) TAOTOKEN: auth success, token expires in 3600s I (895) MQTT: connecting to mqtt.example.com:1883 I (1023) MQTT: connected, session present=0 I (1030) MQTT: subscribed to device/esp32-001/cmd I (1100) REPORT: published to device/esp32-001/report关键节点是TAOTOKEN: auth success和MQTT: connected。如果卡在 auth 阶段,检查统一 Key 是否正确、API 通道是否可达;如果卡在 MQTT 连接,检查会话凭证是否过期、Broker 地址和端口是否正确。
5.2 MQTT 订阅验证
在 PC 或服务器上用 mosquitto_sub 订阅上报主题:
mosquitto_sub -h mqtt.example.com -p 1883 \ -u esp32-001 -P <token> \ -t "device/esp32-001/report" -v正常应该每 10 秒收到一条 JSON:
device/esp32-001/report {"temp":25.3,"hum":58.1,"ts":1710000000}如果订阅端收不到数据,但串口日志显示 publish 成功,检查主题是否匹配、QoS 是否一致、Broker 是否做了 ACL 限制。
5.3 断线重连验证
手动重启路由器或断开 WiFi,观察串口日志:
I (5000) wifi: state: run -> init (0x0) I (5001) WIFI: disconnected, retry in 5000ms I (10002) wifi: state: init -> auth (0xb0) I (10300) esp_netif_handlers: sta ip: 192.168.1.105 I (10305) TAOTOKEN: re-auth success I (10400) MQTT: reconnected重连后统一 Key 不需要重新烧录,设备自动重新鉴权并恢复上报。这就是统一 Key 方案的价值:密钥轮换和重连逻辑解耦,设备端只关心「拿到 Key 就去换凭证」。
6. 本篇常见错排查
6.1 鉴权返回 401
最常见的原因是统一 Key 写错或权限范围不匹配。检查控制台里 Key 的状态是否启用、是否绑定了正确的设备批次。另外注意 Key 前后不要有空格,NVS 写入时如果用了strncpy要确保以\0结尾。
6.2 MQTT 连接被拒绝
如果 Broker 返回Connection Refused: not authorised,通常是会话凭证过期或 username/password 不匹配。TaoToken 颁发的会话凭证有有效期(默认 3600 秒),设备需要在过期前重新鉴权。建议在 MQTT 断开回调里触发重新鉴权,而不是直接重连。
6.3 上报数据丢失
QoS 设为 0 时消息可能丢失,物联网上报建议用 QoS 1。另外检查report_interval_ms是否小于 MQTT keepalive,如果上报间隔太长,Broker 可能认为设备离线。一般 keepalive 设为上报间隔的 2 倍以上比较安全。
6.4 串口日志乱码
波特率不匹配是最常见的原因。ESP32 默认 115200,但有些开发板出厂固件用的是 74880。在menuconfig里确认UART console baud rate设置,和串口监视器保持一致。
6.5 NVS 写入失败
如果配网后 settings.json 写不进 NVS,检查分区表是否给 NVS 留了足够空间。默认分区表通常给 NVS 留 24KB,如果存了多套配置可能不够。可以在partitions.csv里调整nvs分区大小。
7. 下一步:把统一 Key 用起来
设备侧链路跑通后,统一 Key 的价值才真正体现出来。你可以在控制台按批次管理 Key,给不同批次的设备分配不同权限;轮换时只需更新一次,所有设备重新鉴权后自动生效。如果后续要接入模型对话能力,可以直接用同一个 Key 调模型对话接口;如果要做长期编码或 Agent 任务,可以了解 Coding Plan 的配额方案。
接入文档里有完整的 API 通道说明和错误码列表,遇到鉴权或上报问题可以先查文档。API Keys 页面可以创建和管理统一 Key,建议按设备批次创建,不要所有设备共用一个 Key,这样出问题时能快速定位到具体批次。