Skip to content

3.11 MQTT 云端通信 ​

这节要搞懂:设备怎么把数据传到云端、云端怎么下发命令。 需要基础:3.10

一、什么是 MQTT ​

MQTT(Message Queuing Telemetry Transport)是一种轻量级的物联网通信协议。设备把数据发布(publish)到某个主题(Topic),关心这个主题的客户端订阅(subscribe)后就能收到。

打个比方:MQTT 就像一个公告板。

  • 设备把温度贴到"温度"那一栏 —— 谁想看谁去看(发布)
  • 你想改闹钟,就往"命令"那一栏贴张纸 —— 设备一直盯着那一栏(订阅)

双方不用互相知道对方是谁,只要认识同一个"栏目名"就行。这叫发布订阅模式。

二、为什么物联网都用它 ​

HTTPMQTT
谁发起只能设备主动问双向都能主动
想下发命令设备得不停轮询服务器直接推
开销每次都带一堆头部极小(最小 2 字节)
连接一次一断长连接

关键差别是"服务器能不能主动找设备" —— HTTP 不行,MQTT 行。这就是远程控制必须用 MQTT 的原因。

三、工程里接的是 OneNET ​

工程用的是中国移动的 OneNET 物联网平台(app_mqtt.c、app_onenet_ota.c)。

在平台上要先建三样东西:

概念是什么
产品一类设备的模板,定义有哪些属性
设备具体的一台,有唯一的设备名
Token连接凭证,相当于密码

Token 怎么来 ​

OneNET 的 MQTT 连接不是简单的用户名密码,而是用产品 ID、设备名、密钥、过期时间按规则算出一个 token 字符串,填在 MQTT 的 password 字段里。

c
esp_mqtt_client_config_t mqtt_config = {
    .broker.address.uri            = "mqtt://mqtts.heclouds.com:1883",
    .credentials.username          = ONENET_PRODUCT_ID,   // 产品 ID
    .credentials.client_id         = ONENET_DEVICE_NAME,  // 设备名
    .credentials.authentication.password = s_onenet_token, // ⭐ 算出来的 token
    .session.keepalive             = 60,
};

Token 有过期时间

生成 token 时要指定一个过期时间戳,过了就连不上了。

设成远一点(比如几年后),或者在设备端定期重新生成。

调试时突然连不上云平台,先看是不是 token 过期了。

四、属性上报:设备 → 云 ​

把当前状态发布到上报主题。工程里实际上报这些字段:

字段含义
temp_value当前温度
humidity_value当前湿度
historical_record历史记录
city天气城市
auto_light自动亮度开关
light当前亮度
maxtemp_set / minitemp_set温度报警上下限
maxhum_set / minihum_set湿度报警上下限

上报的不只是传感器数据

注意里面也包含配置项(city、light、报警阈值……)。

这样做的好处:手机 App 打开时,不用再单独查一次配置 —— 上报的这一条里什么都有了,界面可以直接照着显示。

c
static void mqtt_report_state(void)
{
    app_config_t config;
    app_config_get(&config);

    char payload[512];
    snprintf(payload, sizeof(payload),
             "{\"id\":\"1\",\"params\":{"
             "\"temp_value\":{\"value\":%.1f},"
             "\"humidity_value\":{\"value\":%.1f},"
             "\"city\":{\"value\":\"%s\"},"
             "\"auto_light\":{\"value\":%d},"
             "\"light\":{\"value\":%d},"
             "\"maxtemp_set\":{\"value\":%.1f},"
             "\"minitemp_set\":{\"value\":%.1f}"
             "}}",
             s_temperature, s_humidity,
             config.city, config.auto_light, config.light,
             config.maxtemp_set, config.minitemp_set);

    esp_mqtt_client_publish(s_client, ONENET_TOPIC_PROP_POST,
                            payload, 0,
                            1,          // QoS 1:至少送达一次
                            0);         // 不保留
}

字段名要和平台上定义的物模型一致

OneNET 上建产品时要定义每个属性的标识符(identifier)。

代码里上报的名字必须和平台上定义的一模一样,否则平台收到了也认不出来,会当成非法属性丢掉。

调不通时先去平台后台对一遍字段名。

QoS 选哪个

QoS保证什么时候用
0发出去就不管高频数据,丢一两条无所谓
1至少送达一次(可能重复)⭐ 日常用这个
2恰好一次开销大,很少用

上报频率别太高

每秒一条 = 一天 86400 条,免费额度很快就用完。

1 分钟一次足够;数据变化大时再额外推一条。

五、属性下发:云 → 设备 ​

MQTT 能改哪些配置 ⭐ ​

注意:MQTT 支持的配置项比 BLE 少。 当前工程 app_mqtt.c 的属性下发只处理这 8 项:

配置项BLE(3.10)MQTT
city 天气城市✅✅
auto_light 自动亮度开关✅✅
light 屏幕亮度✅✅
volume 音量✅✅
maxtemp_set 温度上限✅✅
minitemp_set 温度下限✅✅
maxhum_set 湿度上限✅✅
minihum_set 湿度下限✅✅
alarm 闹钟✅❌ 不支持
refresh_minutes 刷新间隔✅❌ 不支持
auto_light_min 亮度下限✅❌ 不支持
auto_light_max 亮度上限✅❌ 不支持

一句话概括两者的关系

MQTT 和 BLE 用的是相同的配置更新思想,但当前工程里 MQTT 只开放了部分配置项,BLE 配置特征值开放的字段更多。

想远程设闹钟,目前要用蓝牙

闹钟目前只能通过 BLE 设置(3.10)。

想让 MQTT 也能设,需要在 app_mqtt.c 的下发处理里补上 alarm 字段的解析 —— 因为下游 app_config_update() 本来就支持,只差解析这一步。

这也说明了统一入口的价值:加一个字段只要改解析,不用动后面任何代码。

代码 ​

c
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_mqtt_client_subscribe(s_client, ONENET_TOPIC_PROP_SET, 1);
        break;

    case MQTT_EVENT_DATA: {
        // 收到:{"params":{"light":60,"volume":70}}
        cJSON *root = cJSON_ParseWithLength(event->data, event->data_len);
        if (root == NULL) {
            break;
        }

        cJSON *params = cJSON_GetObjectItem(root, "params");
        if (cJSON_IsObject(params)) {

            app_config_t config;
            app_config_get(&config);

            cJSON *city = cJSON_GetObjectItem(params, "city");
            if (cJSON_IsString(city)) {
                strncpy(config.city, city->valuestring,
                        sizeof(config.city) - 1);
            }

            cJSON *light = cJSON_GetObjectItem(params, "light");
            if (cJSON_IsNumber(light)) {
                config.light = light->valueint;
            }

            cJSON *volume = cJSON_GetObjectItem(params, "volume");
            if (cJSON_IsNumber(volume)) {
                config.volume = volume->valueint;
            }

            // ... auto_light、四个报警阈值同理

            app_config_update(&config);      // ⭐ 和 BLE 汇合到同一个入口
        }

        cJSON_Delete(root);                  // ⭐ 别漏
        break;
    }

    case MQTT_EVENT_DISCONNECTED:
        ESP_LOGW(TAG, "mqtt disconnected");  // 客户端会自动重连
        break;
    }
}

订阅要放在 CONNECTED 事件里

不能在 esp_mqtt_client_start() 之后马上订阅 —— 那时候还没连上,订阅会失败。

断线重连之后订阅也会失效,所以每次 MQTT_EVENT_CONNECTED 都要重新订阅一遍。

回调里别干重活

这个回调跑在 MQTT 客户端自己的任务上,卡住它整个云端通信都会停。

只解析 + 调 app_config_update(),然后立刻返回(和 BLE 回调、中断服务函数是同一个原则)。

六、统一入口的价值 ⭐ ​

设备的"设置"分三类,各走各的链路:

【第一类:运行配置】城市、亮度、音量、闹钟、报警阈值……

  BLE 配置特征值 ──► app_config_update_from_json() ──┐
                                                     ├──► app_config_update()
  MQTT 属性下发  ─────────────────────────────────────┘         │
                                                                ▼
                                                        写入 NVS(配置)
                                                                │
                                                                ▼
                                                          配置变化回调
                                                                │
                                                                ▼
                                                     s_config_event_queue
                                                                │
                                                                ▼
                                                       config_apply_task
                                                                │
                                                                ▼
                                                             改硬件

【第二类:WiFi 凭据】ssid / password

  HTTP 网页配网 ──► app_wifi_provision() / save_wifi() ──► NVS(wifi_cfg 命名空间)
  BLE WiFi 特征值 ─┘

【第三类:SD 卡文件操作】

  HTTP 网页 ──► app_sd / app_audio_player / app_image_viewer

别把三类混为一谈

它们走的是完全不同的链路,存的也是不同的 NVS 命名空间。

存到哪经过谁
运行配置配置命名空间app_config_update() → 配置回调 → config_apply_task
WiFi 凭据wifi_cfg 命名空间app_wifi_provision() / save_wifi()
SD 卡操作不存配置,直接操作文件app_sd 等模块

WiFi 账号密码不走 app_config_update() —— 它有自己的保存路径。混在一起讲,学员会以为改 WiFi 和改亮度是同一条链路。

运行配置这一类确实汇合了

MQTT 和 BLE 用的是同一套配置更新思想 —— 解析完各自的格式之后,都调 app_config_update()。

汇合之后,写 NVS、通知、应用到硬件这一整套只有一份代码。

判断架构好不好,就看这个

加一个新的控制方式,要改几个地方?

这里加第四种(语音、红外遥控),只要它最终调到 app_config_update(),下游一行都不用动。

本节要点 ​

  • MQTT = 发布订阅,像公告板,双方不用互相认识
  • 相比 HTTP,服务器能主动推消息给设备 —— 这是远程控制的关键
  • 工程接的是 OneNET,要先在平台上建产品、设备,算出 token
  • token 有过期时间,连不上先查它
  • 上报用 QoS 1,1 分钟一次
  • 订阅必须放在 MQTT_EVENT_CONNECTED 里,断线重连后要重新订阅
  • ⭐ MQTT 只开放 8 项配置,闹钟、刷新间隔、自动亮度上下限目前只能用 BLE 改
  • 上报字段是 temp_value / humidity_value / city / light 等,名字要和平台物模型一致
  • ⭐ 回调里只解析 + 调 app_config_update(),和 BLE 汇合到同一条链路

操作过程 ​

目标:设备连上 OneNET,在网页后台看到它上报的温湿度,并从后台改一个配置。

第 1 步:在 OneNET 上建产品和设备 ​

  1. 注册并登录 OneNET 控制台
  2. 创建产品,接入协议选 MQTT
  3. 在产品下创建设备,记下:
要记的用在哪
产品 IDMQTT 的 username
设备名称MQTT 的 client_id
设备密钥生成 token 用

第 2 步:定义物模型(重要) ​

在产品里添加属性,标识符要和代码里上报的字段完全一致:

temp_value      温度
humidity_value  湿度
city            城市
auto_light      自动亮度
light           亮度
maxtemp_set     温度上限
minitemp_set    温度下限
maxhum_set      湿度上限
minihum_set     湿度下限
volume          音量

字段名对不上就白干

平台收到不认识的属性会直接丢掉,你在后台什么都看不到,还以为是设备没连上。

调不通时先来这里对一遍字段名。

第 3 步:生成 token 填进代码 ​

用 OneNET 提供的 token 生成工具(或按文档算),把产品 ID、设备名、密钥、过期时间填进去生成一串 token。

c
#define ONENET_PRODUCT_ID   "你的产品ID"
#define ONENET_DEVICE_NAME  "你的设备名"
#define ONENET_TOKEN        "version=2018-10-31&res=products%2F..."

过期时间设远一点

设成几年后。token 过期会导致 MQTT 和 OTA 一起失效,而且报错不明显。

第 4 步:编译烧录 ​

第 5 步:应该看到什么 ​

I (8000) mqtt: MQTT_EVENT_CONNECTED          ← ① 连上了
I (8010) mqtt: subscribed to prop/set        ← ② 订阅成功
I (68000) mqtt: report: {"id":"1","params":{"temp_value":{"value":26.5}...

OneNET 后台:设备状态变成在线,属性列表里能看到温湿度的值在更新。

第 6 步:从后台下发一个配置 ​

在 OneNET 后台找到"属性下发",下发:

json
{"light": 30}

串口:

I (90000) mqtt: MQTT_EVENT_DATA
I (90010) config: light updated: 80 -> 30

屏幕应该变暗。

第 7 步:试试 MQTT 不支持的字段 ​

下发 {"alarm":"07:30"},你会发现没反应 —— 因为当前工程 MQTT 只处理 8 个字段,闹钟不在其中(见第五节)。

这不是 bug,是没实现

想让它支持,在 app_mqtt.c 的下发处理里补上 alarm 字段的解析就行,下游代码完全不用动。

可以当成一个练习来做。

动手试试 ​

  1. 在 OneNET 上建好产品和设备,跑通一次连接,看串口打印 MQTT_EVENT_CONNECTED。
  2. 从平台下发一次亮度设置,在 app_config_update() 里打日志确认收到了。
  3. 分别用 BLE 和 MQTT 各改一次亮度,验证两条路走的是同一个入口。
  4. 试着给 app_mqtt.c 补上 alarm 字段的解析,让 MQTT 也能设闹钟。
  5. 拔掉网线让 MQTT 断开,观察它多久自动重连上。