要在 OneNet 平台实现完整的设备控制,核心是搭建「下行指令解析(平台→设备)+ 上行属性上报(设备→平台)+ MQTT 主题订阅(确保消息互通)+ 响应回执(确保消息可靠)」的完整闭环。

一、订阅 OneNet MQTT 协议的核心主题(必须订阅)

OneNet 平台基于 MQTT 协议通信,所有设备控制和数据交互都依赖于指定主题的订阅与发布,你必须提前订阅对应主题,否则无法实现消息互通。

(1)订阅属性设置主题:

         接收OneNet下发的设备属性控制指令(平台→设备)

         主题:$sys/{pid}/{device-name}/thing/property/set    

         参数填写: id 为下行数据的 id 一致,code 为 200 代表成功,msg 可以自定义。

        为何对于这个主题:设备需订阅这个主题,才能接收平台下发的下行控制指令(如开灯、打开蜂鸣器),执行完成后需反馈响应平台。

(2)订阅属性上报的响应主题:

        接收OneNet对设备属性上报的响应 (设备→平台→平台返回是否收到数据)

        主题: $sys/{pid}/{device-name}/thing/property/post/reply

        参数填写: id 为下行数据的 id 一致,code 为 200 代表成功,msg 可以自定义。

        为何对于这个主题:设备完成平台下发的控制指令(如开灯)并反馈响应平台,需上报此时最新状态至平台更新同步;通过订阅对应主题接收平台响应,确认状态同步成功。

 (3)订阅主题代码示例


//订阅OneNet平台的系统主题

void onenet_subscribe(void)
{
    char topic[128];  // MQTT主题缓冲区

    // 订阅属性上报的回复主题:接收OneNet对设备属性上报的响应
    snprintf(topic, 128, "$sys/%s/%s/thing/property/post/reply", ONENET_PRODUCT_ID, ONENET_DEVICE_NAME);
    // 订阅该主题(QoS=1,确保消息至少送达一次)
    esp_mqtt_client_subscribe_single(mqtt_handle, topic, 1);  //(参数1:MQTT客户端句柄;参数2:待订阅主题;参数3:QoS等级1)

    // 订阅属性设置主题:接收OneNet下发的设备属性控制指令
    snprintf(topic, 128, "$sys/%s/%s/thing/property/set", ONENET_PRODUCT_ID, ONENET_DEVICE_NAME);
    // 订阅该主题(QoS=1)
    esp_mqtt_client_subscribe_single(mqtt_handle, topic, 1);
}  

二、解析设备属性设置下行数据(平台→设备)

完整流程

  1. MQTT 消息回调,筛选下行属性设置主题

            编写 onenet_property_handle_dm()  解析并执行 OneNET 平台向设备下发的「属性设置指令」,设备收到 MQTT 消息后,先判断消息是否来自「属性设置下行主题」,只有匹配才进行解析。
  2. 调用你的解析函数,处理属性指令

            将消息 payload 转换为 cJSON 对象,传入你现有的解析函数,处理如亮度、RGB、蜂鸣器、风扇等属性。
  3. 生成执行结果回执,准备发布到响应主题

            解析并执行完成后,构建回执 JSON,告知平台指令执行结果。
     

关键代码(MQTT 消息回调 + 解析对接)

// 解析并执行 OneNET 平台向设备下发的「属性设置指令」
onenet_property_handle_dm(property);


//响应(执行结果回执发布到响应主题)

// 从JSON对象property中,根据字段名"id"获取对应的cJSON节点句柄
cJSON* id_js = cJSON_GetObjectItem(property,"id"); 

// 对 OneNET 下行指令的「ACK 回执响应」
if (id_js != NULL && cJSON_IsString(id_js)) 
{
    // 向平台返回ACK响应(200=处理成功,描述信息"success")
    // 从JSON对象中获取"id"字段句柄,再提取id字符串值传入ACK函数(保证与平台下发的id一致)
    onenet_property_ack(cJSON_GetStringValue(id_js),200,"success"); //响应 onenet 平台
} else {
    ESP_LOGE(TAG, "Property set message missing valid 'id' field");
}

设备属性设置请求和响应 JSON 格式

        按照onenet平台下发 设备属性设置请求json 格式解析,并执行设置指令(操控硬件)

        按照onenet平台给的 设备属性设置响应 json 格式来编写响应内容,响应平台执行完成

下行指令(OneJSON):   云端先向设备发送 property/set(下行指令)

请求Topic:  $sys/{pid}/{device-name}/thing/property/set

OneJSON数据格式:

{
  "id": "123",
  "version": "1.0",
  "params": {
    "temperature":"30.5"
  }
}

请求参数描述

参数	类型	    描述
id	    String	    消息id号,用户自定义,String类型的数字,长度限制不超过13位。
version	String	    物模型版本号,可选字段,不填默认为1.0。
params	JsonObject	属性设置参数。如以上示例中,设置属性:{"temperature":"30.5" }

​—————————————————————————————————————————————————————————————————————————————————————————

上行响应(OneJSON):  设备处理完成后,向云端返回 property/set_reply(上行响应)

响应Topic:  $sys/{pid}/{device-name}/thing/property/set_reply

OneJSON数据格式:

{
    "id":"123",
    "code":200,
    "msg":"xxxx"
}

响应参数描述

参数	类型	  描述
id	    String	  消息id号,与平台下发一致,String类型的数字,长度限制不超过13位。
code	Integer	  结果状态码
msg	    String	  错误信息

​

注意:OneJSON下发 设备属性设置的 JSON 格式(扁平格式 或 标准格式)

在实际项目中,你无法完全控制平台下发的格式,原因如下:

  1. 平台升级风险:如果你的设备只兼容扁平格式,当平台升级到新版并强制下发标准格式时,设备会解析失败,导致控制中断。
  2. 多场景适配:你的设备可能同时被「调试工具」和「规则引擎」调用,前者下发扁平格式,后者下发标准格式。
  3. 第三方集成:如果设备需要对接多个平台或第三方系统,不同系统的格式可能不一致,兼容两种格式能避免对接失败。
  •  扁平格式示例
{
  "id": "12345",
  "version": "1.0",
  "params": {
    "Brightness": {
      "value": 80
    },
    "Buzzer": {
      "value": true
    },
    "Fan": {
      "value": false
    },
    "RGBColor": {
      "value": {
        "Red": 255,
        "Green": 0,
        "Blue": 0
      }
    }
  }
}
  • 标准格式(带 value 层级)示例
{
  "id": "12345",
  "version": "1.0",
  "params": {
    "Brightness": 80,
    "Buzzer": true,
    "Fan": false,
    "RGBColor": {
      "Red": 255,
      "Green": 0,
      "Blue": 0
    }
  }
}

同时兼容两种格式,这里我通过 get_effective_node() 实现了兼容,应对格式的不确定性

        实现兼容格式是否带 value 层级,最终获取最终有效节点都可以直接取值

// 核心工具函数:兼容带/不带 value 层级,获取最终有效节点
static cJSON* get_effective_node(cJSON* src_node, const char* key)
{
    // 步骤 1:优先尝试提取 value 子节点(兼容带 value 格式)
    cJSON* effective_node = cJSON_GetObjectItem(src_node, key);
    
    // 步骤 2:若 value 提取失败(NULL),直接使用源节点(兼容不带 value 格式)
    if (effective_node == NULL)
    {
        effective_node = src_node;
    }
    
    // 步骤 3:返回最终有效节点(可能是 value 子节点,也可能是源节点)
    return effective_node;
}

    三、设备属性上报(设备→平台)

    设备需要主动向平台上报当前属性状态(如亮度、RGB 颜色、蜂鸣器开关),确保平台显示的状态与设备实际状态一致,核心是「构建标准上报 JSON → 发布到上报主题」。

    完整流程

    1. 构建上报 JSON 数据:

      编写 onenet_property_upload_dm() 函数,
      生成符合 OneNet 标准的属性 JSON(带 value 层级,格式统一)。
      cJSON* onenet_property_upload_dm()
      {
      
          // 1. 创建JSON根节点对象(最外层{}),所有数据均嵌套在该根对象内
          cJSON* root = cJSON_CreateObject();  // root :根节点
          
          // 2. 向根节点添加 字符串类型字段"id",值为"123"(设备/请求唯一标识,可自定义为动态ID)
          cJSON_AddStringToObject(root,"id","123");         // 向 root 对象中 添加  键值对(字符串类型)
          cJSON_AddStringToObject(root,"version","1.0");    // 向 root 对象中 添加  键值对(字符串类型) 
      
          // 3. 向根节点添加 嵌套对象字段"params"(核心属性集合),并保存该对象句柄用于后续操作
          cJSON* params_js = cJSON_AddObjectToObject(root,"params");  // 向 root 对象中 添加对象 params
      
          // 向对象 params 添加子节点
      
          // 灯亮度
          cJSON* brightness_js = cJSON_AddObjectToObject(params_js,"Brightness"); // 向 params_js 对象中 添加对象 Brightness
          cJSON_AddNumberToObject(brightness_js,"value",brightness); // 向 brightness_js 对象中 添加键值对(整数类型)
      
          // RGB颜色
          cJSON* color_js = cJSON_AddObjectToObject(params_js,"RGBColor");    // 向 params_js 对象中 添加对象 RGBColor
          cJSON* color_value_js = cJSON_AddObjectToObject(color_js,"value");  // 向 color_js 对象中 添加对象 value
          cJSON_AddNumberToObject(color_value_js,"Red",rgb_red);     // 向 color_value_js 对象中 添加键值对(整数类型)
          cJSON_AddNumberToObject(color_value_js,"Green",rgb_green); // 向 color_value_js 对象中 添加键值对(整数类型)
          cJSON_AddNumberToObject(color_value_js,"Blue",rgb_blue);   // 向 color_value_js 对象中 添加键值对(整数类型)
          
          cJSON* Buzzer_js = cJSON_AddObjectToObject(params_js,"Buzzer"); // 向 params_js 对象中 添加对象 Buzzer
          cJSON_AddBoolToObject(Buzzer_js,"value",buzzer_value); // 向 Buzzer_js 对象中 添加键值对(整数类型)
      
          cJSON* Fan_js = cJSON_AddObjectToObject(params_js,"Fan"); // 向 params_js 对象中 添加对象 Fan
          cJSON_AddBoolToObject(Fan_js,"value",fan_value); // 向 Fan_js 对象中 添加键值对(整数类型)
          // 返回构建完成的JSON根对象句柄,供调用者转换为字符串并上报OneNet平台
          return root;
      }
    2. 转换 JSON 为字符串,发布到上报主题:

      将构建好的 JSON 转换为字符串,通过 MQTT 发布到「属性上报主题」。
       
    3. 接收平台上报响应,确认上报成功:

      通过订阅的「上报响应主题」,确认平台是否成功接收数据。

    关键代码(上报函数调用 + MQTT 发布)

    // 设备向 OneNET 平台的属性主动上报(更新实时数据)
    
    // 1. 构建设备属性上报的JSON对象(包含灯光开关、亮度、RGB等状态)
    cJSON* property_js1 = onenet_property_upload_dm(); 
    
    // 2. 将cJSON对象转为无格式压缩JSON字符串(减小传输数据量,适合网络上报)
    char* data1 = cJSON_PrintUnformatted(property_js1);
    
    // 3. 发布JSON字符串到OneNet平台,完成设备属性最新上报(上行)
    onenet_post_property_data(data1);
    
    // 4. 释放cJSON动态分配的字符串内存,避免内存泄漏
    cJSON_free(data1);
    
    // 5. 释放cJSON根对象内存(递归释放所有嵌套节点)
    cJSON_Delete(property_js1);

    四、订阅与响应的核心价值(为什么必须做)

    这2个订阅(订阅设置下行数据 和 订阅上报成功响应),是确保整个通信闭环可靠、可追溯的关键,缺一不可:

    1. 订阅下行数据:是「设备接收平台控制指令」的唯一途径,不订阅就无法收到平台的控制指令,设备无法被远程控制;
    2. 发布 / 订阅上行响应
      • 对「属性设置」:设备返回回执给平台,平台能标记指令「执行成功 / 失败」,方便问题排查(如指令下发后设备无响应,可通过回执判断是设备未接收还是执行失败);
      • 对「属性上报」:设备接收平台的上报回执,可确认数据是否成功上传,若上传失败可重试,避免平台显示旧状态。


     

      Logo

      DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

      更多推荐