Bootstrap

小熊派Nano接入华为云

一、华为云IoTDA创建产品

创建如下服务,并添加对应的属性和命令。

二、小熊派接入

根据小熊派官方示例代码D6完成了小熊派接入华为云并实现属性上传命令下发。源码:小熊派开源社区/BearPi-HM_Nano

1. MQTT连接代码分析

这部分代码在oc_mqtt.c和oc_mqtt.h中

/*该结构体在oc_mqtt.h中
 *用来存储与MQTT设备相关的认证和标识信息。
*/
struct bp_oc_info
{
    char client_id[OC_CLIENT_ID_LEN];
    char username[OC_USERNAME_LEN];
    char password[OC_PASSWORD_LEN];
    char user_device_id_flg;
};
typedef struct bp_oc_info *bp_oc_info_t;

/*该函数在oc_mqtt.c中
 *在调用mqtt连接前,通过该函数对连接参数进行赋值
*/
void device_info_init(char *client_id, char * username, char *password)
{
    oc_info.user_device_id_flg = 1;
    strncpy(oc_info.client_id,  client_id, strlen(client_id));
    strncpy(oc_info.username,     username, strlen(username));
    strncpy(oc_info.password,  password, strlen(password));
}

/*该函数在oc_mqtt.c中
 *调用该函数可以完成mqtt的连接
 *其中oc_mqtt_entry函数将根据云端地址进行连接,然后连接mqtt
*/
int oc_mqtt_init(void)
{
    int result = 0;

    if (init_ok)
    {
        //LOG_D("oc mqtt already init!");
        return 0;
    }
    if (oc_mqtt_entry() < 0)
    {
        result = -2;
        goto __exit;
    }
    __exit:
    if (!result)
    {
        //LOG_I("oc package(V%s) initialize success.", oc_SW_VERSION);
        init_ok = 1;//官网这里为0,根据逻辑这里应该为连接成功,连接成功后应该置1避免重复连接。
    }
    else
    {
        //LOG_E("oc package(V%s) initialize failed(%d).", oc_SW_VERSION, result);
    }
    return result;
}

2. 属性上报

华为云IoTDA中,属性上报格式如下:

{
    "services": [{
            "service_id": "xxxxx",//服务ID为产品创建后添加的服务
            "properties": {
                "temp": 23//属性和对应的值
            }
        }]
}

在小熊派源码中通过结构体封装了属性上报的函数,调用方便代码分析如下

typedef struct
{
   void *nxt;
   char *service_id;                         ///< the service id in the profile, which could not be NULL
   char *event_time;                         ///< eventtime, which could be NULL means use the platform time
   oc_mqtt_profile_kv_t *service_property;   ///< the property in the profile, which could not be NULL
}oc_mqtt_profile_service_t;

该结构体位于oc_mqtt.h中,用于表示接入云端的一个服务内容,具体分析如下:

  • void *nxt;是一个指向下一个oc_mqtt_profile_service_t结构体的指针,用于实现服务的链表结构。通过这个字段,可以将多个服务链接在一起。
  • char *service_id;:是一个指向字符的指针,表示服务的ID。在配置文件中,服务的ID是必需的,不能为空(NULL)。
  • char *event_time;:是一个指向字符的指针,表示事件的时间。这个字段可以是NULL,表示使用平台的时间。
  • oc_mqtt_profile_kv_t *service_property;:是一个指向oc_mqtt_profile_kv_t结构体的指针,表示服务的属性。需要上报的属性。
typedef struct
{
    void                 *nxt;   ///< ponit to the next key
    char                 *key;
    en_oc_profile_data_t  type;
    void                 *value;
}oc_mqtt_profile_kv_t;

typedef enum
{
    EN_OC_MQTT_PROFILE_VALUE_INT = 0,
    EN_OC_MQTT_PROFILE_VALUE_LONG,
    EN_OC_MQTT_PROFILE_VALUE_FLOAT,
    EN_OC_MQTT_PROFILE_VALUE_STRING,           ///< must be ended with '\0'
    EN_OC_MQTT_PROFILE_VALUE_LAST,
}en_oc_profile_data_t;

该结构体位于oc_mqtt.h中,用于存储服务的属性它包含以下字段:

  • void *nxt;:是一个指向下一个oc_mqtt_profile_kv_t结构体的指针,用于实现键值对的链表结构。通过这个字段,可以将多个键值对链接在一起。
  • char *key;:这是一个指向字符的指针,表示键的名称。
  • en_oc_profile_data_t type;:这是一个枚举类型,表示值的类型。枚举en_oc_profile_data_t定义了多种数据类型,用于指定与键相关联的值的类型。
  • void *value;:这是一个指向任意类型数据的指针,表示与键相关联的值。由于value的类型是void*,它可以是任何类型的数据,具体类型由type字段指定。

通过这两个结构体构建上报属性的消息更加方便,能够动态添加属性。属性上报代码如下:

/*该函数位于iot_cloud_oc_sample.c中,将需要上报的属性进行初始化*/
static void deal_report_msg(report_t *report)
{
    oc_mqtt_profile_service_t service;
    oc_mqtt_profile_kv_t fish_temp;
    oc_mqtt_profile_kv_t fish_light;
    oc_mqtt_profile_kv_t fish_pump;
    oc_mqtt_profile_kv_t fish_heat;

    service.event_time = NULL;
    service.service_id = "HomeBox";
    service.service_property = &fish_temp;
    service.nxt = NULL;

    fish_temp.key = "FishTemp";
    fish_temp.value = &report->temp;
    fish_temp.type = EN_OC_MQTT_PROFILE_VALUE_INT;
    fish_temp.nxt = &fish_light;


    fish_light.key = "FishLight";
    fish_light.value = g_app_cb.light? "ON" : "OFF";
    fish_light.type = EN_OC_MQTT_PROFILE_VALUE_STRING;
    fish_light.nxt = &fish_pump;

    fish_pump.key = "FishPump";
    fish_pump.value = g_app_cb.pump ? "ON" : "OFF";
    fish_pump.type = EN_OC_MQTT_PROFILE_VALUE_STRING;
    fish_pump.nxt = &fish_heat;

    fish_heat.key = "FishHeat";
    fish_heat.value = g_app_cb.heat ? "ON" : "OFF";
    fish_heat.type = EN_OC_MQTT_PROFILE_VALUE_STRING;
    fish_heat.nxt = NULL;

    oc_mqtt_profile_propertyreport(USERNAME, &service);
    return;
}

其中oc_mqtt_profile_propertyreport函数位于oc_mqtt.h中,该函数是一个用于构建和发布 MQTT 消息,上报设备服务属性。该函数中间接调用了oc_mqtt_profile_package.c文件中的oc_mqtt_profile_propertyrepormake_servicesmake_servicemake_kvsprofile_fmtvalue函数,这些函数协同工作,以 JSON 格式创建服务属性,并通过 MQTT 发布。

int oc_mqtt_profile_propertyreport(char *deviceid,oc_mqtt_profile_service_t *payload)
{
    int ret = (int)en_oc_mqtt_err_parafmt;
    char *topic;
    char *msg;

    if(NULL == deviceid)
    {
        if(NULL == s_oc_mqtt_profile_cb.device_id)
        {
            return ret;
        }
        else
        {
            deviceid = s_oc_mqtt_profile_cb.device_id;
        }
    }

    if((NULL== payload) || (NULL== payload->service_id) || (NULL == payload->service_property))
    {
        return ret;
    }

    topic = topic_make(CN_OC_MQTT_PROFILE_PROPERTYREPORT_TOPICFMT, deviceid,NULL);
    msg = oc_mqtt_profile_package_propertyreport(payload);

    printf("msg:%s \r\n",msg);

    if((NULL != topic) && (NULL != msg))
    {
        ret = oc_mqtt_publish(topic,(uint8_t *)msg,strlen(msg),(int)en_mqtt_al_qos_1);
    }
    else
    {
        ret = (int)en_oc_mqtt_err_sysmem;
    }

    free(topic);
    free(msg);

    return ret;
}

oc_mqtt_profile_propertyreport -->oc_mqtt_profile_package_propertyreport(创建上报信息的json对象)–>make_services(创建json对象数组)–>make_service(创建属性json对象)–>make_kvs(创建属性json数组)–>profile_fmtvalue(创建各个属性内容的json对象)

  • oc_mqtt_profile_propertyreport函数
    • 这个函数负责构建并发布一个 MQTT 消息,该消息包含设备的服务属性报告。
    • 它首先检查deviceidpayload是否为NULL。如果是,则根据全局回调函数中的设备 ID 或返回错误。
    • 使用 topic_make 函数构建 MQTT 主题。
    • 使用oc_mqtt_profile_package_propertyreport 函数打包服务属性报告为消息。
    • 使用oc_mqtt_publish函数(同样未在代码中定义)发布 MQTT 消息。
    • 最后,释放分配的内存并返回结果。
  • make_services函数
    • 这个函数创建一个 JSON 数组,该数组包含多个服务对象的 JSON 表示。
    • 它遍历传入的service_info链表,为每个服务调用make_service函数,并将结果添加到 JSON 数组中。
    • 如果在内存分配过程中发生错误,它会跳转到EXIT_MEM标签,释放已分配的资源,并返回NULL
  • make_service函数
    • 这个函数创建一个 JSON 对象,该对象表示单个服务。
    • 它添加service_id、properties(使用make_kvs函数生成)和可选的event_time到 JSON 对象中。
    • 如果在内存分配过程中发生错误,它会跳转到EXIT_MEM`标签,释放已分配的资源,并返回NULL。
  • make_kvs函数:
    • 这个函数创建一个 JSON 对象,该对象包含键值对的列表,这些键值对表示服务的属性。
    • 它遍历传入的kvlst链表,为每个键值对调用profile_fmtvalue函数,并将结果添加到 JSON 对象中。
    • 如果在内存分配过程中发生错误,它会跳转到EXIT_MEM标签,释放已分配的资源,并返回NULL。
  • profile_fmtvalue函数:
    • 这个函数根据键值对的类型(整数、长整数、浮点数或字符串)创建一个相应的 JSON 值。
    • 它返回创建的 JSON 值,该值可以是数字或字符串。

3. 消息接收

在mqtt连接后,oc_mqtt.c文件中oc_mqtt_entry函数中设置了mqtt的回调函数mq_client.defaultMessageHandler = mqtt_callback;,在函数mqtt_callback中将接收到的值存入结构体oc_mqtt.cmd_rsp_cb中后续进行处理。

/*主函数*/
oc_set_cmd_rsp_cb(oc_cmd_rsp_cb);

void oc_cmd_rsp_cb(uint8_t *recv_data, size_t recv_size, uint8_t **resp_data, size_t *resp_size)
{
    app_msg_t *app_msg;

    int ret = 0;
    app_msg = malloc(sizeof(app_msg_t));
    app_msg->msg_type = en_msg_cmd;
    app_msg->msg.cmd.payload = (char *)recv_data;

    printf("recv data is %.*s\n", recv_size, recv_data);
    ret = osMessageQueuePut(mid_MsgQueue, &app_msg, 0U, 0U);
    if (ret != 0)
    {
        free(recv_data);
    }
    *resp_data = NULL;
    *resp_size = 0;
}


/*oc_mqtt.c*/
void oc_set_cmd_rsp_cb(void (*cmd_rsp_cb)(uint8_t *recv_data, uint32_t recv_size, uint8_t **resp_data, uint32_t *resp_size))
{
    oc_mqtt.cmd_rsp_cb = cmd_rsp_cb;
}
  • 函数 oc_set_cmd_rsp_cb</font>

这个函数用于设置命令响应的回调函数。它接收一个参数:

  • void (*cmd_rsp_cb)(uint8_t *recv_data, uint32_t recv_size, uint8_t **resp_data, uint32_t *resp_size):这是一个函数指针,指向命令响应的回调函数。

函数内部逻辑如下:
- 将传入的回调函数 cmd_rsp_cb赋值给 oc_mqtt.cmd_rsp_cboc_mqtt 是一个结构体,用于存储MQTT相关的配置和状态,其中 cmd_rsp_cb成员用于存储命令响应的回调函数。

  • 回调函数oc_cmd_rsp_cb
    这个函数是命令响应的回调函数,当接收到命令时,这个函数会被调用。它接收四个参数:
    • uint8_t *recv_data:指向接收到的数据的指针。
    • size_t recv_size:接收到的数据的大小。
    • uint8_t **resp_data:指向响应数据的指针的地址,用于返回响应数据。
    • size_t *resp_size>:指向响应数据大小的指针,用于返回响应数据的大小。

接收到的消息存入消息队列进行处理

;