Skip to content

设备接入指南

本文档说明如何将 ESP32 等嵌入式设备接入 YunStack-IoT 平台。所有示例基于 ESP-IDF 框架。


1. 接入流程


2. 设备认证

设备连接 EMQX 需要以下凭证(固件烧录时预置)。设备必须预先在平台注册(数据库中已存在记录),否则 EMQX PostgreSQL 认证插件将拒绝连接:

凭证来源说明
ClientIdWiFi MAC 地址MQTT 连接标识,12 位十六进制大写
UsernameProductKey产品标识,如 p_relay_4ch
PasswordDeviceSecret设备密钥,每台唯一,烧录时写入

认证机制: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 桌面客户端,用于调试设备通信:

  1. 创建连接,填写 EMQX 地址和端口
  2. 订阅 sys/+/+/thing/event/# 查看所有设备上报
  3. 手动发布消息到 sys/{pid}/{did}/thing/service/property/set 模拟控制指令
  4. 验证 Payload 格式和 Topic 是否正确

串口调试

bash
# 查看 ESP32 串口输出(波特率 115200)
idf.py -p /dev/ttyUSB0 monitor

# 或使用 screen
screen /dev/ttyUSB0 115200

# 或使用 minicom
minicom -D /dev/ttyUSB0 -b 115200

EnvSense-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. 常见问题

设备频繁断连

  1. 检查 WiFi 信号强度(RSSI < -70dBm 时考虑调整天线位置)
  2. MQTT keepalive 建议 60~120 秒
  3. 确认 EMQX 未达到连接数上限

消息丢失

  1. 使用 QoS 1(至少一次送达),不要用 QoS 0
  2. 设备端重连后重新订阅必要的下行 Topic
  3. 关键上报(如状态变更)在重连成功后重新发送

数据格式错误

  1. 确保 params 中属性名与物模型 id 完全一致
  2. bool 类型用 JSON true/false,不要用 1/0
  3. 数值类型直接写数字,不要用字符串

注册失败

  1. 确认 ProductKey 与平台上创建的产品 ID 一致
  2. 确认 DeviceSecret 格式正确(UUID 格式)
  3. 查看 EMQX Dashboard 认证日志