实现了Mqtt的链接后,需要与华为云IoTDA进行属性上报、命令解析、命令应答等。需要参照华为云官网给出的API参考使用前必读_设备接入 IoTDA;
1. 华为云函数构建
1.1. 设备属性上报
该功能就是用于设备按产品模型中定义的格式将属性数据上报给平台,最通常的温度信息设备状态等,上报后上位机可以通过设备影子消息来获取上报的属性。
Topic
上行:$oc/devices/{device_id}/sys/properties/report
参数说明
字段名 |
必选/可选 |
类型 |
参数描述 |
services |
必选 |
List<ServiceProperty> |
设备服务数据列表(具体结构参考下表ServiceProperty定义表)。 |
ServiceProperty结构定义:
字段名 |
必选/可选 |
类型 |
参数描述 |
service_id |
必选 |
String |
设备的服务ID,由创建的产品模型确定。 |
properties |
必选 |
Object |
设备服务的属性列表,具体字段在设备关联的产品模型中定义,可以设置多个字段。 |
event_time |
可选 |
String |
设备采集数据UTC时间(格式可选:秒级别:yyyyMMdd'T'HHmmss'Z',毫秒级别:yyyy-MM-dd'T'HH:mm:ss.SSS'Z'),如:20161219T114920Z或者2020-08-12T12:12:12.333Z。 设备上报数据不带该参数或参数格式错误时,则数据上报时间以平台时间为准。 |
官方示例
Topic: $oc/devices/{device_id}/sys/properties/report
数据格式:
{
"services": [{
"service_id": "Temperature",
"properties": {
"value": 57,
"value2": 60
},
"event_time": "20151212T121212Z"
},
{
"service_id": "Battery",
"properties": {
"level": 80,
"level2": 90
},
"event_time": "20151212T121212Z"
}
]
}
event_time项为可选项,在实现过程中没有选用
封装函数
共封装了两个函数分别为”Mqtt_Json_Publish“,”Mqtt_Number_Publish“分别向属性中传输int型数字和Json结构,由于项目中除了4G通信还有蓝牙通信的方式所以有的属性类型直接设置成了json对象,如果需要传输其他类型只需对应修改构建json;
#define TOPIC_UP ("$oc/devices/{device_id}/sys/properties/report")
/**
* @brief 组装json数据并发布
* @param 属性的json字符串,string:属性名,data:属性内容json
* @retval 0:构建json失败;1成功
*/
uint8_t Mqtt_Json_Publish(const char *string,cJSON *data)
{
cJSON *root = cJSON_CreateObject(); // 创建根对象
if (root == NULL)
{
printf("Failed to create root JSON object.\n");
return 0;
}
cJSON *services = cJSON_AddArrayToObject(root, "services"); // 在根对象中添加一个名为 "services" 的数组
if (services == NULL)
{
printf("Failed to add services array to root JSON object.\n");
cJSON_Delete(root);
return 0;
}
cJSON *service = cJSON_CreateObject(); // 创建服务对象
if (service == NULL)
{
printf("Failed to create service JSON object.\n");
cJSON_Delete(root);
return 0;
}
cJSON *properties = cJSON_CreateObject(); // 创建属性对象
if (properties == NULL)
{
printf("Failed to create properties JSON object.\n");
cJSON_Delete(service);
cJSON_Delete(root);
return 0;
}
//将服务ID添加到属性对象
cJSON_AddStringToObject(service, "service_id", "id");
// 将环境对象添加到属性对象
cJSON_AddItemToObject(properties, string, data);
// 将属性对象添加到服务对象
cJSON_AddItemToObject(service, "properties", properties);
// 将服务对象添加到服务数组
cJSON_AddItemToArray(services, service);
// 将根对象转换为 JSON 字符串
char *lte_json_body = cJSON_Print(root);
if (lte_json_body == NULL)
{
printf("Failed to print JSON object.\n");
cJSON_Delete(properties);
cJSON_Delete(service);
cJSON_Delete(root);
return 0;
}
if(is_mqtt_connected==1)//mqtt 连接成功
{
// 打印 JSON 字符串
// printf("HUAWEI YUN JSON: %s\n", lte_json_body);
//发送
memset(mqtt_topic,0,TOPIC_LENGTH);
sprintf(mqtt_topic,TOPIC_UP);
Mqtt_Publish(mqtt_topic,(unsigned char *)lte_json_body);
}
// 释放内存
cJSON_Delete(root);
cJSON_free(lte_json_body);
return 1;
}
/**
* @brief 组装json数据并发布
* @param 属性的json字符串,string:属性名,state:属性内容为int
* @retval 0:构建json失败;1成功
*/
uint8_t Mqtt_Number_Publish(const char *string,int state)
{
cJSON *root = cJSON_CreateObject(); // 创建根对象
if (root == NULL)
{
printf("Failed to create root JSON object.\n");
return 0;
}
cJSON *services = cJSON_AddArrayToObject(root, "services"); // 在根对象中添加一个名为 "services" 的数组
if (services == NULL)
{
printf("Failed to add services array to root JSON object.\n");
cJSON_Delete(root);
return 0;
}
cJSON *service = cJSON_CreateObject(); // 创建服务对象
if (service == NULL)
{
printf("Failed to create service JSON object.\n");
cJSON_Delete(root);
return 0;
}
cJSON *properties = cJSON_CreateObject(); // 创建属性对象
if (properties == NULL)
{
printf("Failed to create properties JSON object.\n");
cJSON_Delete(service);
cJSON_Delete(root);
return 0;
}
//将服务ID添加到属性对象
cJSON_AddStringToObject(service, "service_id", "monitor&control");
// 将环境对象添加到属性对象
cJSON_AddNumberToObject(properties, string, state);
// 将属性对象添加到服务对象
cJSON_AddItemToObject(service, "properties", properties);
// 将服务对象添加到服务数组
cJSON_AddItemToArray(services, service);
// 将根对象转换为 JSON 字符串
char *lte_json_body = cJSON_Print(root);
if (lte_json_body == NULL)
{
printf("Failed to print JSON object.\n");
cJSON_Delete(properties);
cJSON_Delete(service);
cJSON_Delete(root);
return 0;
}
if(is_mqtt_connected==1)//mqtt 连接成功
{
// 打印 JSON 字符串
// printf("HUAWEI YUN JSON: %s\n", lte_json_body);
//发送
memset(mqtt_topic,0,TOPIC_LENGTH);
sprintf(mqtt_topic,TOPIC_UP);
Mqtt_Publish(mqtt_topic,(unsigned char *)lte_json_body);
}
// 释放内存
cJSON_Delete(root);
cJSON_free(lte_json_body);
return 1;
}
1.2. 解析平台命令下发
用于平台向设备下发设备控制命令。平台下发命令后,需要设备及时将命令的执行结果返回给平台,如果设备没回响应,平台会认为命令执行超时。
Topic
下行: $oc/devices/{device_id}/sys/commands/request_id={request_id}
上行:$oc/devices/{device_id}/sys/commands/response/request_id={request_id}
说明:
- {request_id}用于唯一标识这次请求。设备侧收到下行请求的topic带该参数时,上行响应的topic需要将该参数值返回给平台。
- 应用通过平台下发设备命令时,平台会生成唯一ID(command_id)用于标识该命令,并返回给应用。同时该唯一标识会通过设备命令下行Topic中的requst_id携带给设备。
- 设备无法提前感知该request_id,在订阅该Topic时请使用通配符“#”来替代“request_id={request_id}”即为:$oc/devices/{device_id}/sys/commands/#。
命令订阅是我们对request_id可以用“#”来替代,但在上报响应式必须使用下发的request_id,因此在下发命令后需要解析对应的request_id。
下行请求参数说明
字段名 |
必选/可选 |
类型 |
参数描述 |
object_device_id |
可选 |
String |
|
service_id |
可选 |
String |
设备的服务ID,在设备关联的产品模型中定义。 |
command_name |
可选 |
String |
设备命令名称,在设备关联的产品模型中定义。 |
paras |
必选 |
Object |
设备命令的执行参数,具体字段在设备关联的产品模型中定义。 |
上行响应参数说明
命令应答的json格式,具体字段在设备关联的产品模型中定义。
字段名 |
必选/可选 |
类型 |
参数描述 |
result_code |
可选 |
Integer |
标识命令的执行结果,0表示成功,其他表示设备执行结果为失败。不带默认为0。 |
response_name |
可选 |
String |
命令的响应名称。 |
paras |
可选 |
Object |
命令的响应参数,具体字段在设备关联的产品模型中定义。 |
下行请求示例
Topic: $oc/devices/{device_id}/sys/commands/request_id={request_id}
数据格式:
{
"object_device_id": "{object_device_id}",
"command_name": "ON_OFF",
"service_id": "WaterMeter",
"paras": {
"value": "1"
}
}
上行响应示例
Topic:$oc/devices/{device_id}/sys/commands/response/request_id={request_id}
数据格式:
{
"result_code": 0,
"response_name": "COMMAND_RESPONSE",
"paras": {
"result": "success"
}
}
命令解析函数封装
int8_t Parse_MqttCmd(uint8_t *data,char * request_id)
{
// 寻找JSON数据的开始位置
const char *json_start = strstr((char *)data, "{");
if (json_start == NULL)
{
printf("JSON data not found in the received string.\n");
return -1;
}
size_t json_length = strlen(json_start);
// 分配内存并复制JSON数据
char *json_data = (char *)malloc(json_length + 1);
if (json_data == NULL) {
printf("Memory allocation failed.\n");
return -1;
}
strncpy(json_data, json_start, json_length);
json_data[json_length] = '\0'; // 添加null终止符
// 检查JSON数据是否完整
if (!is_json_complete(json_data))
{
printf("Received JSON data is incomplete or malformed.\n");
cJSON_free(json_data);
return -1;
}
// 如果完整,则可以进一步解析JSON数据
cJSON *root = cJSON_Parse(json_data);
if (root == NULL)
{
printf("Failed to parse JSON data.\n");
cJSON_free(json_data);
return -1;
}
// ... 在这里处理JSON数据 ...
// 获取并打印"command_name"字段的值
cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "command_name");
//判断"command_name"字段的值选择控制类型
if (name && cJSON_IsString(name) && (name->valuestring != NULL))
{
char * command_name = name->valuestring;
printf("Name: %s\n", command_name);
//判断控制命令
if(strstr(name->valuestring,"light"))
{
cJSON *paras = cJSON_GetObjectItemCaseSensitive(root, "paras");/*获取obj中的paras的json*/
if (paras && cJSON_IsObject(paras))
{
cJSON *sw_item = cJSON_GetObjectItemCaseSensitive(paras, "sw");/*开关状态*/
if (sw_item != NULL && cJSON_IsBool(sw_item))
{
int sw_value = sw_item->valueint; // cJSON使用int来表示bool值
printf("sw: %d\n", sw_value); // 输出sw的值,1代表true,0代表false
//控制命令发送
if(sw_value == 1)
{
//开灯
}else{
//关灯
}
//设备响应
Mqtt_Cmd_Resp(1,request_id);
}else
{
printf("Failed to get 'sw' value or it's not a boolean.\n");
}
}
}
}
// 释放资源
cJSON_Delete(root);
cJSON_free(json_data);
return 1;
}
解析命令时需要从云端下发的数据中提取出一个完整的json对象来,这里通过观察下发的消息后采用从第一个“{”开始截取,通过函数判断json是否完整。
int is_json_complete(const char *json_data)
{
// 检查是否有明确的开始和结束标志
if (json_data == NULL || json_data[0] != '{' || json_data[strlen(json_data) - 1] != '}')
{
printf("\r\n incomplete \r\n");
return 0; // JSON不完整
}
// 尝试解析JSON数据,如果解析失败,则可能不完整
cJSON *root = cJSON_Parse(json_data);
if (root == NULL)
{
printf("Parse error before: %s\n", cJSON_GetErrorPtr());
printf("\r\n malformed \r\n");
return 0; // JSON解析失败,可能不完整
}
cJSON_Delete(root); // 不论完整与否,都需要释放cJSON对象
return 1; // JSON完整
}
上报响应函数封装
上报响应需要先解析下发命令的request_id,在调用解析之前进行处理
void Mqtt_Cmd_Resp(char * request_id)
{
// 创建一个根对象
cJSON *root = cJSON_CreateObject();
// 添加result_code字段
cJSON_AddNumberToObject(root, "result_code", 0);
// 创建一个嵌套的paras对象
cJSON *paras = cJSON_CreateObject();
cJSON_AddStringToObject(paras, "result", "success");
// 将paras对象添加到根对象
cJSON_AddItemToObject(root, "paras", paras);
// 打印JSON内容
char *json_string = cJSON_Print(root);
printf("%s\n", json_string);
memset(mqtt_topic,0,TOPIC_LENGTH);
//组装上报反馈主题
sprintf(mqtt_topic,Topic_CMD_RESP,request_id);
printf("mqtt_topic:%s\r\n", mqtt_topic);
Mqtt_Publish(mqtt_topic,(unsigned char *)json_string);
// 释放分配的内存
cJSON_free(json_string);
cJSON_Delete(root);
}
2. 主函数调用
在主函数调用之前需要了解上一篇文章
/*此代码只为样式调用部分,其他相关初始化为显示*/
int main(void)
{
...//其他初始化
//初始化4G模块
LTE_Init();
while(1)
{
/*采用串口3作为与4G模块的通信串口*/
/*串口3采用DMA+空闲中断进行接收*/
/*中断回调函数等见其他文章*/
/*usart3--接收cat1数据接收*/
if(g_USART3_Recv_Flag)
{
g_USART3_Recv_Flag=0;
/*串口数据解析开始*/
//mqtt未连接
if(is_mqtt_connected == 0)
{
LTE_Mqtt_Connect(data);
}else{
//mqtt连接成功
//判断返回值,返回为发布应答
if(data[0]>>4 == PUBACK)
{
//发布消息成功
//发布消息计数值减1,发布是进行了自加见上一篇文章
mqtt_publish_count--;
if(mqtt_publish_count < 0) mqtt_publish_count = 0;
publish_overtime = 0;
}
printf("------%d\r\n",data[0]>>4);
//判断返回值,返回值为服务器发布订阅消息
if(data[0]>>4 == PUBLISH)//解析
{
//查找订阅消息的中json数据的开头{以及查找request_id
for(int i=0;i<len;i++)
{
/*request_id开始前为“=”,长度为36,截取request_id*/
if(data[i]==0x3D)
{
memset(request_id,0,strlen(request_id));
memcpy(request_id,&data[i+1],36);
printf("Extracted request_id: %s\n", request_id);
}
if(data[i]==0x7b)//判断为“{”
{
memset(mqtt_cmd,0,strlen(mqtt_cmd));
sprintf(mqtt_cmd, "%s", data+i);
/*调用解析函数*/
Parse_MqttCmd((uint8_t *)mqtt_cmd,request_id);
break;
}
}
}
//判断返回值,返回值为ping消息
if(data[0]>>4 == PINGRESP)
{
//发布消息成功
mqtt_ping_count--;
if(mqtt_ping_count < 0) mqtt_ping_count = 0;
ping_overtime = 0;
}
}
/*串口数据解析结束*/
__HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart3, (uint8_t*)g_USART3_Recv_Data, RECV_DATA_MAX_LEN);
}
if(上报属性条件)
{
Mqtt_Json_Publish("属性名",内容);
}
}
}
至此已经能够通过stm32+有人4G模块实现了与华为云的上传下达,后续将介绍上位机APP接口调用,最终形成一个完整demo