1. 程式人生 > 其它 >手把手教你利用鴻蒙OS實現智慧家居·LOT上雲專案

手把手教你利用鴻蒙OS實現智慧家居·LOT上雲專案

手把手教你利用鴻蒙OS實現智慧家居·LOT上雲專案

一、前言

今天使用鴻蒙OS,做一個LOT上雲的智慧家居專案。我們想實現的場景是這樣的:雲端WEB有一個控制介面,能夠操控家房間裡的燈和風扇,同時將房間裡溫度、溼度、光強實時顯示出來。

二、案例思路

先講一下大致的思路,可以分為兩個部分:先配置雲伺服器,再編寫底層MCU的業務程式碼,實現資料採集與聯網上報。邏輯上沒有複雜的東西,但貴在走通整個流程。和普通RTOS上雲的方案差不多,具體差異在軟體方面。

1、準備工作

雲伺服器的配置,不算複雜,主要是前端的處理和顯示,可以先在伺服器調通,再根據雲服務提供的API,進行訪問。

硬體環境使用小熊派·鴻蒙季開發板和E53_IA1擴充套件板(有溫溼度光強感測器和電機)。

具體如何建立工程,可以參考我的上一篇文章,這裡採用Windows環境下的開發方式。無論是基於HPM還是Docker環境獲取鴻蒙原始碼建立工程,都很簡單。

這個Demo,我們將會用到鴻蒙OS的核心子系統和驅動子系統。核心子系統主要使用執行緒相關的API(基於CMSIS-2.0)和網路服務相關的API(socket);驅動子系統主要呼叫底層的GPIO和硬體I2C,控制外部裝置。

在核心子系統和驅動子系統上,我們還需要一個元件(軟體包),物聯網通訊協議MQTT,利用它進行上雲服務。

列一下主要的資源和工具:

硬體:

  • 小熊派 · 鴻蒙季開發板
  • E53_IA1擴充套件板

原始碼:

  • Hi3861開發板的原始碼,來源Hb,適用於windows環境

IDE

  • 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:  -64
no:004, ssid:DIRECT-IXLAPTOP-O3K3OKASmsUK  , rssi:  -69
...
********************
Select:  2 wireless, Waiting...
+NOTICE:CONNECTED
SENSOR:lum:67.50 temp:33.17 hum:68.33
WaitConnectResult:wait success[1]s
WiFi connect succeed!
begain to dhcp
<-- DHCP state:Inprogress -->
<-- DHCP state:Inprogress -->
<-- DHCP state:OK -->
server :
        server_id : 192.168.1.1
        mask : 255.255.255.0, 1
        gw : 192.168.1.1
        T0 : 7200
        T1 : 3600
        T2 : 6300
clients <1> :
        mac_idx mac             addr            state   lease   tries   rto
        0       e81131641696    192.168.1.123   10      0       1       3
SENSOR:lum:79.17 temp:32.77 hum:60.45
SENSOR:lum:38.33 temp:32.51 hum:52.88
SENSOR:lum:42.50 temp:32.30 hum:50.59
SENSOR:lum:42.50 temp:32.11 hum:49.73
SENSOR:lum:40.00 temp:31.91 hum:49.74
SENSOR:lum:41.67 temp:31.75 hum:49.96

回到華為雲平臺,平臺上的裝置顯示為線上狀態

點選裝置右側的“檢視”,進入裝置詳情頁面,可看到上報的資料

在華為雲平臺裝置詳情頁,單擊“命令”,選擇同步命令下發,選中建立的命令屬性,單擊“確定”,即可傳送下發命令控制裝置。

看一下現象:串列埠列印雲端接收的資料,並執行點燈的指令。

5. 除錯華為雲API

點選「API檢索和除錯」,進入API調測介面。

目前開放有Java、python、node.js、php等,可以根據個人的需求,構建前端。這裡我們先除錯API,選擇一個裝置命令,按照圖示操作。
注意Body裡面的引數,與我們上文產品的屬性是一樣的,其中paras的引數,填寫要符合圖片給出的規範,也就是JSON的格式。

最後點選調式,給出除錯結果,我們的開發板上,燈也被點亮!

四、總結

  1. 雲端的操作,要注意和終端軟體編寫的資訊相同,一個是MQTT的連線資訊不能出錯,還有就是注意名稱之間的大小寫要相同;
  2. 終端MCU軟體的編寫,注意分層設計,先寫好各自的功能模組,最後再實現相關的業務邏輯;
  3. 注意調測,利用好串列埠和雲端MQTT資訊跟蹤服務;
  4. 整體走下來,工作量還是蠻大的,需要注意的地方有很多,所以要特別細心。
  5. 原始碼後臺回覆鴻蒙獲取,工程檔案可以參考上個文章獲取。