教你手拉手使用鸿蒙OS实现智能家居·LOT上云项目
一、前言
今日使用鸿蒙OS,做一个LOT上云智能家居项目。我们想要实现的场景是这样的:云WEB有一个控制界面,可以控制房间里的灯和风扇,实时显示房间里的温度、湿度和光强。
二、案例思路
先说一般思路,可分为两部分:先配置云服务器,再编写底层MCU实现数据采集和网上报告的业务代码。逻辑上没有复杂的东西,但整个过程都很重要。RTOS上云的方案差不多,具体区别在于软件。
1、准备工作
云服务器的配置并不复杂,主要是前端处理和显示,可以先在服务器中调通,然后根据云服务提供API,进行访问。
小熊派用于硬件环境·鸿蒙季开发板和E53_IA扩展板(有温湿度光强传感器和电机)。
具体如何创建工程,可以参考我的上一篇文章,这里采用Windows无论是基于环境的发展模式。HPM还是Docker鸿蒙源码创建项目的环境获取非常简单。
这个Demo,我们将使用鸿蒙OS内核子系统和驱动子系统。内核子系统主要采用线程相关API(基于CMSIS-2.0)与网络服务相关的API(socket);驱动子系统主要调用底层GPIO和硬件I2C,控制外部设备。
在核心系统和驱动子系统中,我们还需要一个组件(软件包),物联网通信协议MQTT,利用它进行云服务。
列出主要资源和工具:
- 小熊派 · 鸿蒙季开发板
- E53_IA1扩展板
- Hi3861开发板源代码,来源Hb,适用于windows环境
- vscode(IDE平台)
- DevEco Device Tool (IDE组件,可选)
- RaiDrive
- windows10 64位
- ubuntu18.04
- HUAWEI-LoTCloud(云服务器平台)
- CloudIDE(可选,用于在线调试API接口)
下面,跟随我的具体操作,一步一步地实现整个计划,内容更多,一定要提前安装环境,你可以先看看前面的文章,建立环境。
2. 云端操作
先讲云服务器这里。为了方便验证,我们首选华为云服务器(腾讯云、阿里云也可,原理大同小异)。
操作流程大致如下:
在设备进入华为云平台之前,需要在平台上注册,注册的可以忽略这一步。华为云地址:https://www.huaweicloud.com/
登录后,单击华为云首页控制台
,包含各种云服务的产品进入产品控制终端。
选择云服务器的地点是华为-北京四
。
点击左侧的 服务器
,找到物联网
,选择设备接入IoTDA
并立即使用。或者在搜索输入中 设备接入IoTDA
跳过去。下次选择此服务时,直接点击搜索栏下最近访问的服务,即可快速进入相应的服务,非常方便。
点击产品
,选择创建产品
,填写产品信息。「资源空间」选择默认,「产品名称」在这里填一个Smart_House(根据自己的喜好写一个),「协议类型」选择MQTT就好,「数据格式」为JSON,「厂商名称」填写一口Linux,「设备类型」填写senser。点击确定,创建产品。
弹出产品创建成功的窗口新闻。
点击产品列表「查看」,对设备进行相关操作。 定义服务模型,「服务ID」随意命名,在这里填写Agriculture,「服务类型」填入senser。点击确定,添加服务。 接下来为服务设置属性和命令,这里规定了数据通信的基本格式。
点击「添加属性」,以温度为例,「属性名称」填写Temperature,「属性描述」填写温度,「数据类型」为整型,「访问权限」可读,其余默认。「属性名称」内容要和我们在一起MCU发送的信息保持一致,这里先提一下。 与温度相似,我们依次填写以下内容,不同的是灯和电机,两者「数据类型」是字符串,「长度」为3。下图列出了灯具和其他设备的属性。 然后添加服务命令,单击「添加命令」,依次输入「命令名称」,再点击「新的输入参数」。
新的输入参数与服务属性相似,这里是字符串的数据类型,输入枚举值,用英文逗号分割。 让我们来看看所有的属性和命令,几乎如此:
下去,点击「设备」, 选择「注册设备」填写设备属性, 「所属资源空间」选择默认账户, 「所属产品」选择自己创造的产品, 「设备标识码」填写senser, 「设备名称」填写house,其他人保持默认, 点击确定完成创建。
设备创建成功后,需要保存两个重要信息,即设备ID设备密钥。
设备ID: 60cdaf505f880902bcaa161c_senser 设备密钥: 4a423f69b41806de0d8ed77e145534e7
然后我们使用获得的密钥生成直接连接MQTT所需的ClentID,通过此链接跳转:https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/ https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/
以上是我们云服务器的配置,到此为止,下一步是MCU终端上的软件编写。软件编写完成后,两侧进行联调试验。
三、软件编写
我们用的鸿蒙OS已包含源码MQTT在示例工程中可以轻松找到常用的模块。这里简单说一下目录结构,熟悉整个鸿蒙OS在源码框架上的细节。
这里使用的鸿蒙源码工程由HPM具体源码结构如下: 让我们列一张表,看看每个文件夹具体承担了哪些功能:
编译构建子系统applications | BearPi-HM_Nano开发板应用案例 |
base | 主要采用系统基础服务DFX子系统、启动文件、硬件适配接口等 |
kernel | 内核子系统 |
ohos_bundles | 制造商提供的部分组件和服务 |
third_party | 第三方组件 |
foundation | 系统服务框架子系统,WAN开发 |
headers | 存放main头文件 |
src | 存放 main源文件 |
utils | 公共基础库 |
test | XTS认证子系统 |
vendor | 硬件抽象层 |
build | |
out | 存放编译文件 |
bin | 存放二进制文件 |
适合本文项目的代码示例,在applications文件夹,具体目录为:applications\BearPi\BearPi-HM_Nano\sample\D6_iot_cloud_oc
。这里列一下目录文件结构:
E53_IA1.c | 扩展板驱动 |
oc_mqtt_profile_package.c | 打包和配置MQTT数据 |
oc_mqtt.c | MQTT连接服务 |
wifi_connet.c | wifi连接服务 |
iot_cloud_oc_sample.c | 业务逻辑代码 |
我们主要要用到的API如下,具体实现的细节,可以到源文件里面去阅读。可以分为初始化和数据上传两个部分。
1. 初始化
1)设备信息
void device_info_init(char *client_id, char * username, char *password);
设置设备信息,在调用oc_mqtt_init()前要先设置设备信息
无 | 无 |
0 | 成功 |
-1 | 获得设备信息失败 |
-2 | mqtt 客户端初始化失败 |
2)华为IoT平台 初始化
int oc_mqtt_init(void);
华为IoT平台初始化函数,需要在使用 华为IoT平台 功能前调用。
无 | 无 |
0 | 成功 |
-1 | 获得设备信息失败 |
-2 | mqtt 客户端初始化失败 |
3)设置命令响应函数
void oc_set_cmd_rsp_cb(void(*cmd_rsp_cb)(uint8_t *recv_data, size_t recv_size, uint8_t **resp_data, size_t *resp_size));
设置命令响应回调函数。
recv_data | 接收到的数据 |
recv_size | 数据的长度 |
resp_data | 响应数据 |
resp_size | 响应数据的长度 |
无 | 无 |
2. 数据上传
1)设备消息上报
int oc_mqtt_profile_msgup(char *deviceid,oc_mqtt_profile_msgup_t *payload);
是指设备无法按照产品模型中定义的属性格式进行数据上报时,可调用此接口将设备的自定义数据上报给平台,平台将设备上报的消息转发给应用服务器或华为云其他云服务上进行存储和处理。
deviceid | 设备id |
payload | 要上传的消息 |
0 | 上传成功 |
1 | 上传失败 |
2)设备上报属性数据
int oc_mqtt_profile_propertyreport(char *deviceid,oc_mqtt_profile_service_t *payload);
用于设备按产品模型中定义的格式将属性数据上报给平台。
deviceid | 设备id |
payload | 要上传的消息 |
0 | 上传成功 |
1 | 上传失败 |
3)网关批量上报属性数据
int oc_mqtt_profile_gwpropertyreport(char *deviceid,oc_mqtt_profile_device_t *payload);
用于批量设备上报属性数据给平台。网关设备可以用此接口同时上报多个子设备的属性数据。
deviceid | 设备id |
payload | 要上传的消息 |
0 | 上传成功 |
1 | 上传失败 |
4)属性设置的响应结果
int oc_mqtt_profile_propertysetresp(char *deviceid,oc_mqtt_profile_propertysetresp_t *payload);
deviceid | 设备id |
payload | 消息 |
0 | 上传成功 |
1 | 上传失败 |
5)属性查询响应结果
int oc_mqtt_profile_propertygetresp(char *deviceid,oc_mqtt_profile_propertygetresp_t *payload);
deviceid | 设备id |
payload | 消息 |
0 | 上传成功 |
1 | 上传失败 |
6)将命令的执行结果返回给平台
int oc_mqtt_profile_cmdresp(char *deviceid,oc_mqtt_profile_cmdresp_t *payload); 平台下发命令后,需要设备及时将命令的执行结果返回给平台,如果设备没回响应,平台会认为命令执行超时。
deviceid | 设备id |
payload | 要上传的消息 |
0 | 上传成功 |
1 | 上传失败 |
3. 编写业务逻辑
1)连接平台
准备好上文我们获取的连接信息(ClientId、Username、Password),一个可以上网的WIFI(账户和密码),注意不可以用5G频段。
#define CLIENT_ID "60cdaf505f880902bcaa161c_senser_0_0_2021062002"
#define USERNAME "60cdaf505f880902bcaa161c_senser"
#define PASSWORD "e7f839333a8d3618a975e2626df1462f67202f3f4103080fe8d6f05df0fa7ce3"
WifiConnect("TP-LINK_65A8","0987654321");
device_info_init(CLIENT_ID,USERNAME,PASSWORD);
oc_mqtt_init();
oc_set_cmd_rsp_cb(oc_cmd_rsp_cb);
2)推送数据
当需要上传数据时,需要先拼装数据,然后通过oc_mqtt_profile_propertyreport上报数据。代码示例如下:
/** * @brief 处理上报的数据。 * @details Process the reported data. * @param[in] report 需要上报的数据。The data to be reported. * @return None ***/
static void deal_report_msg(report_t *report)
{
/** 定义服务ID句柄 */
oc_mqtt_profile_service_t service;
/** 定义温度的上报数据句柄 */
oc_mqtt_profile_kv_t temperature;
/** 定义湿度的上报数据句柄 */
oc_mqtt_profile_kv_t humidity;
/** 定义亮度的上报数据句柄 */
oc_mqtt_profile_kv_t luminance;
/** 定义电灯的上报数据句柄 */
oc_mqtt_profile_kv_t led;
/** 定义电机的上报数据句柄 */
oc_mqtt_profile_kv_t motor;
/** 初始化要上报的服务ID数据 */
service.event_time = NULL;
service.service_id = "Agriculture";
service.service_property = &temperature;
service.nxt = NULL;
/** 初始化要上报的温度数据 */
temperature.key = "Temperature";
temperature.value = &report->temp;
temperature.type = EN_OC_MQTT_PROFILE_VALUE_INT;
temperature.nxt = &humidity;
/** 初始化要上报的湿度数据 */
humidity.key = "Humidity";
humidity.value = &report->hum;
humidity.type = EN_OC_MQTT_PROFILE_VALUE_INT;
humidity.nxt = &luminance;
/** 初始化要上报的亮度数据 */
luminance.key = "Luminance";
luminance.value = &report->lum;
luminance.type = EN_OC_MQTT_PROFILE_VALUE_INT;
luminance.nxt = &led;
/** 初始化要上报的电灯数据 */
led.key = "LightStatus";
led.value = g_app_cb.led?"ON":"OFF";
led.type = EN_OC_MQTT_PROFILE_VALUE_STRING;
led.nxt = &motor;
/** 初始化要上报的电机数据 */
motor.key = "MotorStatus";
motor.value = g_app_cb.motor?"ON":"OFF";
motor.type = EN_OC_MQTT_PROFILE_VALUE_STRING;
motor.nxt = NULL;
/** 将属性数据上报给平台 */
oc_mqtt_profile_propertyreport(USERNAME,&service);
return;
}
3)命令接收
华为IoT平台支持下发命令,命令是用户自定义的。接收到命令后会将命令数据发送到队列中,task_main_entry函数中读取队列数据并调用deal_cmd_msg函数进行处理,代码示例如下:
/** * @brief 将命令数据发送到队列。 * @details Send command data to the queue. * @param[in] recv_data 接收的数据 * @param[in] recv_size 接收数据的大小 * @param[in] resp_data 接收的上报数据 * @param[in] resp_size 接收的上报数据的大小 * @return None ***/
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;
}
/** * @brief 线程入口,读取队列数据并处理。 * @details Thread entry, read queue data and process. * @param[in] None * @return None ***/
static int task_main_entry( void )
{
app_msg_t *app_msg;
/** 连接WIFI */
WifiConnect("TP-LINK_65A8","0987654321");
/** 注册设备的连接信息*/
device_info_init(CLIENT_ID,USERNAME,PASSWORD);
/** 初始化MQTT*/
oc_mqtt_init();
oc_set_cmd_rsp_cb(oc_cmd_rsp_cb);
while(1){
app_msg = NULL;
(void)osMessageQueueGet(mid_MsgQueue,(void **)&app_msg,NULL, 0U);
if(NULL != app_msg){
switch(app_msg->msg_type){
case en_msg_cmd:
deal_cmd_msg(&app_msg->msg.cmd);
break;
case en_msg_report:
deal_report_msg(&app_msg->msg.report);
break;
default:
break;
}
free(app_msg);
}
}
return 0;
}
/** * @brief 解析命令,并给出处理的结果。 * @details Thread entry, read queue data and process. * @param[in] cmd 命令。 * @return None ***/
static void deal_cmd_msg(cmd_t *cmd)
{
cJSON *obj_root;
cJSON *obj_cmdname;
cJSON *obj_paras;
cJSON *obj_para;
int cmdret = 1;
oc_mqtt_profile_cmdresp_t cmdresp;
obj_root = cJSON_Parse(cmd->payload);
if(NULL == obj_root){
goto EXIT_JSONPARSE;
}
obj_cmdname = cJSON_GetObjectItem(obj_root,"command_name");
if(NULL == obj_cmdname){
goto EXIT_CMDOBJ;
}
if(0 == strcmp(cJSON_GetStringValue(obj_cmdname),"Agriculture_Control_light")){
obj_paras = cJSON_GetObjectItem(obj_root,"paras");
if(NULL == obj_paras){
goto EXIT_OBJPARAS;
}
obj_para = cJSON_GetObjectItem(obj_paras,"light");
if(NULL == obj_para){
goto EXIT_OBJPARA;
}
///< operate the LED here
if(0 == strcmp(cJSON_GetStringValue(obj_para),"ON")){
g_app_cb.led = 1;
Light_StatusSet(ON);
printf("Light On!");
}
else{
g_app_cb.led = 0;
Light_StatusSet(OFF);
printf("Light Off!");
}
cmdret = 0;
}
else if(0 == strcmp(cJSON_GetStringValue(obj_cmdname),"Agriculture_Control_Motor")){
obj_paras = cJSON_GetObjectItem(obj_root,"paras");
if(NULL == obj_paras){
goto EXIT_OBJPARAS;
}
obj_para = cJSON_GetObjectItem(obj_paras,"motor");
if(NULL == obj_para){
goto EXIT_OBJPARA;
}
///< operate the Motor here
if(0 == strcmp(cJSON_GetStringValue(obj_para),"ON")){
g_app_cb.motor = 1;
Motor_StatusSet(ON);
printf("Motor On!");
}
else{
g_app_cb.motor = 0;
Motor_StatusSet(OFF);
printf("Motor Off!");
}
cmdret = 0;
}
EXIT_OBJPARA:
EXIT_OBJPARAS:
EXIT_CMDOBJ:
cJSON_Delete(obj_root);
EXIT_JSONPARSE:
///< do the response
cmdresp.paras = NULL;
cmdresp.request_id = cmd->request_id;
cmdresp.ret_code = cmdret;
cmdresp.ret_name = NULL;
(void)oc_mqtt_profile_cmdresp(NULL,&cmdresp);
return;
}
4. 编译调试
修改 applications\sample\BearPi\BearPi-HM_Nano
路径下 BUILD.gn 文件,指定 oc_mqtt
参与编译。
#"D1_iot_wifi_sta:wifi_sta",
#"D2_iot_wifi_sta_connect:wifi_sta_connect",
#"D3_iot_udp_client:udp_client",
#"D4_iot_tcp_server:tcp_server",
#"D5_iot_mqtt:iot_mqtt",
"D6_iot_cloud_oc:oc_mqtt",
#"D7_iot_cloud_onenet:onenet_mqtt",
示例代码编译烧录代码后,按下开发板的RESET按键,通过串口助手查看日志,会打印温湿度及光照强度信息。
sdk ver:Hi3861V100R001C00SPC025 2020-09-03 18:10:00
FileSystem mount ok.
wifi init success!
00 00:00:00 0 68 D 0/HIVIEW: hilog init success.
00 00:00:00 0 68 D 0/HIVIEW: log limit init success.
00 00:00:00 0 68 I 1/SAMGR: Bootstrap core services(count:3).
00 00:00:00 0 68 I 1/SAMGR: Init service:0x4b8040 TaskPool:0xfa9a4
00 00:00:00 0 68 I 1/SAMGR: Init service:0x4b8064 TaskPool:0xfb014
00 00:00:00 0 68 I 1/SAMGR: Init service:0x4b81c8 TaskPool:0xfb1d4
00 00:00:00 0 100 I 1/SAMGR: Init service 0x4b8064 <time: 0ms> success!
00 00:00:00 0 0 I 1/SAMGR: Init service 0x4b8040 <time: 0ms> success!
00 00:00:00 0 200 D 0/HIVIEW: hiview init success.
00 00:00:00 0 200 I 1/SAMGR: Init service 0x4b81c8 <time: 0ms> success!
00 00:00:00 0 200 I 1/SAMGR: Initialized all core system services!
00 00:00:00 0 0 I 1/SAMGR: Bootstrap system and application services(count:0).
00 00:00:00 0 0 I 1/SAMGR: Initialized all system and application services!
00 00:00:00 0 0 I 1/SAMGR: Bootstrap dynamic registered services(count:0).
SENSOR:lum:107.50 temp:33.34 hum:63.95
<--System Init-->
<--Wifi Init-->
register wifi event succeed!
callback function for wifi scan:0, 0
+NOTICE:SCANFINISH
callback function for wifi scan:1, 24
WaitSacnResult:wait success[1]s
********************
no:001, ssid:养只狗叫瑞邦 , rssi: -53
no:002, ssid:电信302 , rssi: -63
no:003, ssid:412 , rssi: -