设备接入指南
本文档说明如何将 ESP32 等嵌入式设备接入 YunStack-IoT 平台。所有示例基于 ESP-IDF 框架。
1. 接入流程
2. 设备认证
设备连接 EMQX 需要以下凭证(固件烧录时预置)。设备必须预先在平台注册(数据库中已存在记录),否则 EMQX PostgreSQL 认证插件将拒绝连接:
| 凭证 | 来源 | 说明 |
|---|---|---|
ClientId | WiFi MAC 地址 | MQTT 连接标识,12 位十六进制大写 |
Username | ProductKey | 产品标识,如 p_relay_4ch |
Password | DeviceSecret | 设备密钥,每台唯一,烧录时写入 |
认证机制:EMQX 通过 PostgreSQL 认证插件直连数据库,查询
device表验证 ClientId + Username + Password 是否匹配,无需后端 HTTP 回调。
3. ESP-IDF 项目结构
推荐的项目结构:
firmware/
├── CMakeLists.txt # 顶层项目文件
├── main/
│ ├── CMakeLists.txt # idf_component_register(SRCS "main.c")
│ └── main.c # app_main 入口
├── components/ # 自定义组件
│ └── <sensor>/ # 传感器驱动
│ ├── CMakeLists.txt
│ ├── <sensor>.c
│ └── include/<sensor>.h
└── sdkconfig # IDF 配置4. MQTT 客户端连接
使用 ESP-IDF 内置 esp_mqtt 组件:
c
#include "mqtt_client.h"
static esp_mqtt_client_handle_t mqtt_client;
static void mqtt_event_handler(void *arg, esp_event_base_t base,
int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = event_data;
switch (event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT 已连接");
// 订阅下行 Topic
esp_mqtt_client_subscribe(mqtt_client,
"sys/" PRODUCT_KEY "/" device_id "/thing/service/property/set", 1);
esp_mqtt_client_subscribe(mqtt_client,
"sys/" PRODUCT_KEY "/" device_id "/thing/config/set", 1);
break;
case MQTT_EVENT_DATA:
// 处理收到的消息
handle_mqtt_message(event->topic, event->data, event->data_len);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "MQTT 断开,自动重连中...");
break;
}
}
void mqtt_init(void)
{
// 获取 MAC 地址作为 ClientId
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
char client_id[32];
snprintf(client_id, sizeof(client_id),
"%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtt://your-mqtt-host:1883",
.credentials = {
.client_id = client_id,
.username = PRODUCT_KEY, // 固件预置
.authentication.password = DEVICE_SECRET, // 固件预置
},
};
mqtt_client = esp_mqtt_client_init(&cfg);
esp_mqtt_client_register_event(mqtt_client,
ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_mqtt_client_start(mqtt_client);
}5. 数据上报
5.1 标准上报格式
json
// Topic: sys/{pid}/{did}/thing/event/property/post
// QoS: 1
{
"msgId": "abc123",
"params": {
"relay_1": true,
"relay_2": false,
"relay_3": false,
"relay_4": true,
"chip_temp": 45.2
}
}5.2 四路继电器上报示例
c
void relay_report_properties(void)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "msgId", generate_msg_id());
cJSON *params = cJSON_AddObjectToObject(root, "params");
cJSON_AddBoolToObject(params, "relay_1", relay_states[0]);
cJSON_AddBoolToObject(params, "relay_2", relay_states[1]);
cJSON_AddBoolToObject(params, "relay_3", relay_states[2]);
cJSON_AddBoolToObject(params, "relay_4", relay_states[3]);
cJSON_AddNumberToObject(params, "chip_temp", temp);
char *json_str = cJSON_PrintUnformatted(root);
char topic[128];
snprintf(topic, sizeof(topic),
"sys/%s/%s/thing/event/property/post", PRODUCT_KEY, device_id);
esp_mqtt_client_publish(mqtt_client, topic, json_str, 0, 1, 0);
cJSON_Delete(root);
free(json_str);
}5.3 环境传感器(EnvSense-Box)上报示例
c
void sensor_report_properties(void)
{
// 读取各传感器
float temp = -999, humi = -999, pressure = -999;
float lux = -999, co2 = -999;
aht20_read_data(aht20, &temp, &humi);
bmp280_read_pressure(bmp280, &pressure);
bh1750_get_data(bh1750, &lux);
sc8co2_read_co2(sc8co2, &co2);
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "msgId", generate_msg_id());
cJSON *params = cJSON_AddObjectToObject(root, "params");
cJSON_AddNumberToObject(params, "temperature", temp);
cJSON_AddNumberToObject(params, "humidity", humi);
cJSON_AddNumberToObject(params, "pressure", pressure);
cJSON_AddNumberToObject(params, "light_intensity", lux);
cJSON_AddNumberToObject(params, "co2_concentration", co2);
char *json_str = cJSON_PrintUnformatted(root);
char topic[128];
snprintf(topic, sizeof(topic),
"sys/%s/%s/thing/event/property/post", PRODUCT_KEY, device_id);
esp_mqtt_client_publish(mqtt_client, topic, json_str, 0, 1, 0);
free(json_str);
cJSON_Delete(root);
}EnvSense-Box 完整固件见 GitHub - EnvSense-Box
5.4 上报时机
| 时机 | 行为 |
|---|---|
| 状态变化 | 立即上报(如继电器切换) |
| 周期采集 | 建议 2~60 秒,取决于传感器类型 |
| 首次上线 | 上报完整初始状态 |
| 收到查询指令 | 上报当前状态 |
6. 指令接收与响应
6.1 接收指令
json
// 设备订阅: sys/{pid}/{did}/thing/service/property/set
// Payload:
{
"msgId": "12345",
"params": {
"relay_1": true
}
}6.2 指令响应
json
// 设备回复: sys/{pid}/{did}/thing/service/property/set_reply
{
"msgId": "12345",
"code": 200,
"msg": "success"
}6.3 ESP-IDF 处理示例
c
void handle_mqtt_message(char *topic, int topic_len,
char *data, int data_len)
{
char payload[256] = {0};
memcpy(payload, data, data_len < 255 ? data_len : 255);
cJSON *root = cJSON_Parse(payload);
if (!root) return;
char *msg_id = cJSON_GetObjectItem(root, "msgId")->valuestring;
if (strstr(topic, "/thing/service/property/set")) {
// 解析指令
cJSON *params = cJSON_GetObjectItem(root, "params");
cJSON *relay = cJSON_GetObjectItem(params, "relay_1");
if (relay) {
set_relay(1, cJSON_IsTrue(relay));
}
// 回复确认
send_reply(msg_id, 200, "success");
// 同步上报最新状态
relay_report_properties();
}
cJSON_Delete(root);
}
void send_reply(const char *msg_id, int code, const char *msg)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "msgId", msg_id);
cJSON_AddNumberToObject(root, "code", code);
cJSON_AddStringToObject(root, "msg", msg);
char *json = cJSON_PrintUnformatted(root);
char topic[128];
snprintf(topic, sizeof(topic),
"sys/%s/%s/thing/service/property/set_reply",
PRODUCT_KEY, device_id);
esp_mqtt_client_publish(mqtt_client, topic, json, 0, 1, 0);
free(json);
cJSON_Delete(root);
}7. 本地调试工具
MQTTX
MQTTX 是跨平台 MQTT 桌面客户端,用于调试设备通信:
- 创建连接,填写 EMQX 地址和端口
- 订阅
sys/+/+/thing/event/#查看所有设备上报 - 手动发布消息到
sys/{pid}/{did}/thing/service/property/set模拟控制指令 - 验证 Payload 格式和 Topic 是否正确
串口调试
bash
# 查看 ESP32 串口输出(波特率 115200)
idf.py -p /dev/ttyUSB0 monitor
# 或使用 screen
screen /dev/ttyUSB0 115200
# 或使用 minicom
minicom -D /dev/ttyUSB0 -b 115200EnvSense-Box 通过 UART0 输出 JSON 数据行,可直接在串口终端看到上报内容。
EMQX Dashboard
访问 http://<host>:18083,在 WebSocket 客户端页面可实时查看设备消息、模拟指令发布。
8. 物模型创建
设备在平台的 Web 后台注册时,需提供物模型 JSON。具体规范见 物模型设计规范。
示例 — 四路继电器物模型片段:
json
{
"version": "1.0",
"deviceType": "relay_controller",
"properties": [
{
"id": "relay_1", "name": "继电器1", "type": "bool",
"access": "rw", "ui": { "component": "switch" }
}
]
}9. 常见问题
设备频繁断连
- 检查 WiFi 信号强度(RSSI < -70dBm 时考虑调整天线位置)
- MQTT keepalive 建议 60~120 秒
- 确认 EMQX 未达到连接数上限
消息丢失
- 使用 QoS 1(至少一次送达),不要用 QoS 0
- 设备端重连后重新订阅必要的下行 Topic
- 关键上报(如状态变更)在重连成功后重新发送
数据格式错误
- 确保 params 中属性名与物模型
id完全一致 - bool 类型用 JSON
true/false,不要用1/0 - 数值类型直接写数字,不要用字符串
注册失败
- 确认 ProductKey 与平台上创建的产品 ID 一致
- 确认 DeviceSecret 格式正确(UUID 格式)
- 查看 EMQX Dashboard 认证日志
