一、项目介绍基于OpenHarmony使用HI3861实现空气质量的检测和上传(具有独立APP)采集空气温湿度数据
采集空气中PM2.5的数据
具有WEB配网功能
与服务器进行连接并实现数据交互
可使用清洁能源(太阳能板进行供电和充电)
开发基于OpenHarmony的控制APP
具有离线屏幕显示功能(OLED-0.96寸)
二、WEB配网教程视频:https://www.bilibili.com/video/BV19L411M75o(1)碰一碰配网介绍
通过一机一码的形式,识别到
NFC后云端验证设备,进行弹窗拉起,再由NAN或AP的方式,实现发送配网的SSID和Password。
NAN配网AP配网(2)WEB配网本章主要讲述如何实现web配网,是在STA模式下,模拟为一个网站服务器,当手机或其它设备进行访问时,检测是否为浏览器的协议头(HTTP),返回一个封装好的网页界面,通过网页上输入框的填写实现配网。
HTTP协议介绍:(3)WEB配网界面html源代码如下
实现的效果如下:
(4)soft模式下实现网页服务器该部分步骤分为四步:打开WIFI、进入softap模式,创建tcp服务器,解析HTTP指令。此处可参照
润和开源项目:
https://gitee.com/hihopeorg/HarmonyOS-IoT-Application-Development/tree/master
1)打开WIFI - ret = hi_wifi_init(APP_INIT_VAP_NUM, APP_INIT_USR_NUM);
- if (ret != HISI_OK) {
- printf("wifi init failed!n");
- } else {
- printf("wifi init success!n");
- }
2)进入softap模式- 在softap.c文件下WifiAPTask函数,注册回调
- //注册wifi事件的回调函数
- g_wifiEventHandler.OnHotspotStaJoin = OnHotspotStaJoinHandler;
- g_wifiEventHandler.OnHotspotStaLeave = OnHotspotStaLeaveHandler;
- g_wifiEventHandler.OnHotspotStateChanged = OnHotspotStateChangedHandler;
- error = RegisterWifiEvent(&g_wifiEventHandler);
3)创建socket通道后进入判断接受内容循环- while (1)
- {
- if ((ret = recv(new_fd, recvbuf, sizeof(recvbuf), 0)) == -1)
- {
- printf("recv error rn");
- }else
- {
- //printf("recv :%srn", recvbuf);
- //返回s1中包含s2所有字符的最大起始段长度
- //size_t strspn(const char *s1, const char *s2);
- char* p= strstr(recvbuf,TEST);
- uint16_t DIR_buff = p - recvbuf;
- printf("rnThe GET HTTP num:%drn",DIR_buff);
- if(DIR_buff<10)
- {
- Set_clint_flag = 1;
- }else if(DIR_buff>40)
- {
- Set_clint_flag = 2;
- char *p1, *p2;
- p1 = strstr(recvbuf, "ssid=");
- p2 = strstr(recvbuf, "&password");
- if(p1!=0 && p2!=0 && p1
- {
- p1 += strlen("ssid=");
- memcpy(get_ssid, p1, p2 - p1);
- printf("rnget the ssid = %srn", get_ssid);
- }
- p1 = strstr(recvbuf, "password=");
- p2 = strstr(recvbuf, "&tcp_ip");
- if(p1!=0 && p2!=0 && p1
- {
- p1 += strlen("password=");
- memcpy(get_pwd, p1, p2 - p1);
- printf("get the ssid = %srn", get_pwd);
- }
- WifiConnect(get_ssid,get_pwd);
- }else
- {
- Set_clint_flag = 3;
- }
-
- bzero(recvbuf, sizeof(recvbuf));
- //close(new_fd);
- }
- sleep(2);
- if(Set_clint_flag==1)
- {
-
- if ((ret = send(new_fd, httphard1, strlen(httphard1), 0)) == -1)
- {
- perror("send : ");
-
- }
- if ((ret = send(new_fd, webtr, strlen(webtr), 0)) == -1)
- {
- perror("send : ");
-
- }
-
- Set_clint_flag = 0;
- new_fd = -1;
- break;
- }else if(Set_clint_flag==2)
- {
- Set_clint_flag = 0;
- new_fd = -1;
- WifiConnect(get_ssid,get_pwd);
- break;
- }else if(Set_clint_flag==3)
- {
- Set_clint_flag = 0;
- new_fd = -1;
- break;
- }
- sleep(2);
- }
在这个循环中实现了判断当前是否为HTTP指令,如果接收到访问信号就回发网页具体内容,实现手机显示网页。
在填写SSID和PWD后点击提交,此时手机再向HI3861发出HTTP指令,中间携带填入的信息,该部分由以下程序读取:
- p1 = strstr(recvbuf, "ssid=");
- p2 = strstr(recvbuf, "&password");
- if(p1!=0 && p2!=0 && p1
- {
- p1 += strlen("ssid=");
- memcpy(get_ssid, p1, p2 - p1);
- printf("rnget the ssid = %srn", get_ssid);
- }
此时得到帐号密码后尝试连接,即实现网页配网- WifiConnect(get_ssid,get_pwd);
三、外设驱动本系统使用到usart(PM2.5传感器)、IIC(OLED显示屏)、单总线(DHT11)三个部分和TCP(双线程收发)几个部分Winodows下HI3861开发:https://www.bilibili.com/video/BV1PY41147z8HI3861:鸿蒙网页显示传感器数据:https://www.bilibili.com/video/BV1L34y1k7im(1)打开外设使能在usr_config.mk文件中去掉注释
- CONFIG_I2C_SUPPORT=y
- CONFIG_UART0_SUPPORT=y
(2)OLED显示屏驱动
OLED,即有机发光二极管(Organic Light-Emitting Diode),又称为有机电激光显示(Organic Electroluminesence Display)。OLED由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。
该传感器使用的IIC协议,经过IIC使能后初始化OLED就可以使用了:
- hi_io_set_func(HI_IO_NAME_GPIO_13, HI_IO_FUNC_GPIO_13_I2C0_SDA);
- hi_io_set_func(HI_IO_NAME_GPIO_14, HI_IO_FUNC_GPIO_14_I2C0_SCL);
- ret = hi_i2c_deinit(HI_I2C_IDX_0);
- ret |= hi_i2c_init(HI_I2C_IDX_0, 100000);
- if (ret != HI_ERR_SUCCESS) {
- printf("IIC errorn");
- }else
- {
- printf("IIC suceseffuln");
- }
- OLED_ColorTurn(0);//0正常显示,1 反色显示
- OLED_DisplayTurn(0);//0正常显示 1 屏幕翻转显示
其中主要用到的函数是void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size1):
- //在指定位置显示一个字符,包括部分字符
- //x:0~127
- //y:0~63
- //size:选择字体 12/16/24
- //取模方式 逐列式
- void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size1)
- {
- u8 i,m,temp,size2,chr1;
- u8 y0=y;
- size2=(size1/8+((size1%8)?1:0))*(size1/2); //得到字体一个字符对应点阵集所占的字节数
- chr1=chr-' '; //计算偏移后的值
- for(i=0;i
- {
- //temp=asc2_1206[chr1][i];
- if(size1==12)
- {temp=asc2_1206[chr1][i];} //调用1206字体
- else if(size1==16)
- {temp=asc2_1608[chr1][i];} //调用1608字体
- else return;
- for(m=0;m<8;m++) //写入数据
- {
- if(temp&0x80)OLED_DrawPoint(x,y);
- else OLED_ClearPoint(x,y);
- temp<<=1;
- y++;
- if((y-y0)==size1)
- {
- y=y0;
- x++;
- break;
- }
- }
- }
- }
(3)PM2.5传感器驱动
经过选型,选择使用ZPH02粉尘传感器,该传感器使用USART即可驱动,其协议如下:
我们只需要读取低脉冲的整数部分和小数部分即可,首先初始化USART,读取每次传来的第4和第五个数据即可,整合下进行显示
- /*****************************************************************************
- 宏定义
- *****************************************************************************/
- #define WRITE_BY_INT
- #define UART_DEMO_TASK_STAK_SIZE 2048
- #define UART_DEMO_TASK_PRIORITY 25
- #define DEMO_UART_NUM HI_UART_IDX_2
- #define UART_BUFF_SIZE 108
- /*****************************************************************************
- 函数声明
- *****************************************************************************/
- static hi_void *uart_demo_task(hi_void *param);
- hi_void uart_demo(hi_void);
- hi_u8 GET_PM25;
- static hi_void *uart_demo_task(hi_void *param)
- {
- hi_u8 uart_buff[UART_BUFF_SIZE] = {0};
- hi_u8 *uart_buff_ptr = uart_buff;
- hi_unref_param(param);
- printf("Initialize uart demo successfully, please enter some datas via DEMO_UART_NUM port...n");
- for (;;) {
- hi_s32 len = hi_uart_read(DEMO_UART_NUM, uart_buff_ptr, UART_BUFF_SIZE);
- if (len > 0) {
- #ifdef WRITE_BY_INT
-
- OLED_ShowChar(50,0,(unsigned char)(uart_buff_ptr[3] / 10 % 10 + '0'),16);
- OLED_ShowChar(58,0,(unsigned char)(uart_buff_ptr[3] % 10 + '0'),16);
- OLED_ShowChar(66,0,(unsigned char)(uart_buff_ptr[4] / 10 % 10 + '0'),16);
- OLED_ShowChar(74,0,(unsigned char)(uart_buff_ptr[4] % 10 + '0'),16);
- GET_PM25 = uart_buff_ptr[3]*100 + uart_buff_ptr[4];
- OLED_ShowChar(40,40,(unsigned char)(GET_H / 10 % 10 + '0'),16);
- OLED_ShowChar(48,40,(unsigned char)(GET_H % 10 + '0'),16);
- OLED_ShowChar(40,20,(unsigned char)(GET_T / 10 % 10 + '0'),16);
- OLED_ShowChar(48,20,(unsigned char)(GET_T % 10 + '0'),16);
- OLED_Refresh();
- #else
- hi_uart_write_immediately(DEMO_UART_NUM, uart_buff_ptr, len);
- #endif
- } else {
- printf("Read nothing!n");
- hi_sleep(1000); /* sleep 1000ms */
- }
- }
- hi_task_delete(g_uart_demo_task_id);
- g_uart_demo_task_id = 0;
- return HI_NULL;
- }
- hi_void uart_demo(hi_void)
- {
- hi_u32 ret;
- hi_uart_attribute uart_attr = {
- .baud_rate = 38400, /* baud_rate: 115200 */
- .data_bits = 8, /* data_bits: 8bits */
- .stop_bits = 1,
- .parity = 0,
- };
- /* Initialize uart driver */
- ret = hi_uart_init(DEMO_UART_NUM, &uart_attr, HI_NULL);
- if (ret != HI_ERR_SUCCESS) {
- printf("Failed to init uart! Err code = %dn", ret);
- return;
- }
-
- /* Create a task to handle uart communication */
- hi_task_attr attr = {0};
- attr.stack_size = UART_DEMO_TASK_STAK_SIZE;
- attr.task_prio = UART_DEMO_TASK_PRIORITY;
- attr.task_name = (hi_char*)"uart_demo";
- ret = hi_task_create(&g_uart_demo_task_id, &attr, uart_demo_task, HI_NULL);
- if (ret != HI_ERR_SUCCESS) {
- printf("Falied to create uart demo task!n");
- }
- }
(4)DHT11传感器驱动
DHT11 数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,内部由一个 8 位
单片机控制一个电阻式感湿
元件和一个 NTC 测温元件。DHT11 虽然也是采用单总线协议,但是该协议与 DS18B20 的单总线协议稍微有些不同之处。
相比于 DS18B20 只能测量温度,DHT11 既能检测温度又能检测湿度,不过 DHT11 的精度和测量范围都要低于 DS18B20,其温度测量范围为 0~50℃,误差在±2℃;湿度的测量范围为 20%~90%RH(Relative Humidity 相对湿度—指空气中水汽压与饱和水汽压的百分比),误差在±5%RH。DHT11
电路很简单,只需要将 Dout 引脚连接单片机的一个 I/O 即可,不过该引脚需要上拉一个 5K 的电阻,DHT11 的供电电压为 3~5.5V。
因为使用的是单总线协议,其驱动程序如下:
- /****************************************
- 设置端口为输出
- *****************************************/
- void DHT11_IO_OUT(void)
- {
- //设置GPIO_11为输出模式
- GpioSetDir(DHT11_GPIO, HI_GPIO_DIR_OUT);
- }
- //等待DHT11的回应
- //返回1:未检测到DHT11的存在
- //返回0:存在
- u8 DHT11_Check(void)
- {
- hi_gpio_value gpio_val;
- u8 retry=0;
- GpioSetDir(DHT11_GPIO, HI_GPIO_DIR_IN);//配置为输入模式
- hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val);
- while (gpio_val&&retry<100)//DHT11会拉低40~80us
- {
- hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val);
- retry++;
- hi_udelay(1);
- };
- if(retry>=100)return 1;
- else retry=0;
- hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val);
- while ((!gpio_val)&&retry<100)//DHT11拉低后会再次拉高40~80us
- {
- hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val);
- retry++;
- hi_udelay(1);
- };
- if(retry>=100)return 1;
- return 0;
- }
- //初始化DHT11的IO口 DQ 同时检测DHT11的存在
- //返回1:不存在
- //返回0:存在
- u8 DHT11_Init(void)
- {
- //设置GPIO_11的复用功能为普通GPIO
- IoSetFunc(DHT11_GPIO, HI_IO_FUNC_GPIO_0_GPIO);
- //设置GPIO_11为输出模式
- GpioSetDir(DHT11_GPIO, HI_GPIO_DIR_OUT);
- //设置GPIO_11输出高电平
- GpioSetOutputVal(DHT11_GPIO, 1);
- DHT11_Rst(); //复位DHT11
- return DHT11_Check();//等待DHT11的回应
- }
- //复位DHT11
- void DHT11_Rst(void)
- {
- DHT11_IO_OUT(); //SET OUTPUT
- DHT11_DQ_OUT_Low; //拉低DQ
- hi_udelay(20000);//拉低至少18ms
- DHT11_DQ_OUT_High; //DQ=1
- hi_udelay(35); //主机拉高20~40us
- }
- //从DHT11读取一个位
- //返回值:1/0
- u8 DHT11_Read_Bit(void)
- {
- hi_gpio_value gpio_val;
- u8 retry=0;
- hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val);
- while(gpio_val&&retry<100){//等待变为低电平
- hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val);
- retry++;
- hi_udelay(1);
- }
- retry=0;
- while((!gpio_val)&&retry<100){//等待变高电平
- hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val);
- retry++;
- hi_udelay(1);
- }
- hi_udelay(40);//等待40us //用于判断高低电平,即数据1或0
- hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val);
- if(gpio_val)return 1; else return 0;
- }
- //从DHT11读取一个字节
- //返回值:读到的数据
- u8 DHT11_Read_Byte(void)
- {
- u8 i,dat;
- dat=0;
- for (i=0;i<8;i++)
- {
- dat<<=1;
- dat|=DHT11_Read_Bit();
- }
- return dat;
- }
- //从DHT11读取一次数据
- //temp:温度值(范围:0~50°)
- //humi:湿度值(范围:20%~90%)
- //返回值:0,正常;1,读取失败
- u8 DHT11_Read_Data()
- {
- u8 buf[5]={ 0 };
- u8 i;
- DHT11_Rst();
- if(DHT11_Check()==0)
- {
- for(i=0;i<5;i++)//读取40位数据
- {
- buf[i]=DHT11_Read_Byte();
- }
- if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])//数据校验
- {
- GET_H = buf[0];
- GET_T = buf[2];
- }
- }else return 1;
- return 0;
- }
(4)数据发送和接收因为HI3861的线程限制,这边使用双线程,一个实现TCP数据的发送,另一个实现TCP数据的接收
发送线程:- void TcpClientTest(const char* host, unsigned short port)
- {
- ssize_t retval = 0;
- int sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP socket
- SET_SOCKET_ID = sockfd;
- struct sockaddr_in serverAddr = {0};
- serverAddr.sin_family = AF_INET; // AF_INET表示IPv4协议
- serverAddr.sin_port = htons(port); // 端口号,从主机字节序转为网络字节序
- if (inet_pton(AF_INET, host, &serverAddr.sin_addr) <= 0) { // 将主机IP地址从“点分十进制”字符串 转化为 标准格式(32位整数)
- printf("inet_pton failed!rn");
- goto do_cleanup;
- }
- // 尝试和目标主机建立连接,连接成功会返回0 ,失败返回 -1
- if (connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
- printf("connect failed!rn");
- goto do_cleanup;
- }
- printf("connect to server %s success!rn", host);
- Wifi_SOCKET_GET();
- while (1)
- {
- osDelay(500);
- /////////////////////////////////////////////////////////上传函数
- retval = send(sockfd, buff, 6,0);//其中buff为数据
- }
- do_cleanup:
- printf("do_cleanup...rn");
- closesocket(sockfd);
- }
接收处理线程:- static BOOL Wifi_SOCKET_RUN(void)
- {
- ssize_t retval = 0;
- while(1)
- {
- retval = recv(SET_SOCKET_ID, &response, sizeof(response), 0);
- if(retval>0)
- {
- response[retval] = '\0';
- if(response[0] == 'o')
- {
- printf("send open!rn");//此处对接收到的数据进行处理,并执行对应内容
- }
- }
- }
- do_cleanup:
- printf("do_cleanup...rn");
- closesocket(SET_SOCKET_ID);
- }
- void Wifi_SOCKET_GET(void)
- {
- osThreadAttr_t attr;
- attr.name = "Wifi_SOCKET_RUN";
- attr.attr_bits = 0U;
- attr.cb_mem = NULL;
- attr.cb_size = 0U;
- attr.stack_mem = NULL;
- attr.stack_size = 2048;
- attr.priority = 25;
- if (osThreadNew((osThreadFunc_t)Wifi_SOCKET_RUN, NULL, &attr) == NULL)
- {
- printf("Falied to create WifiAPTask!rn");
- }
- }
四、APP开发(1)环境搭建
我这边用的是今年三月份的版本,不过不影响,界面没什么变化
(2)TCP数据交互- import socket from '@ohos.net.socket';
- let tcp = socket.constructTCPSocketInstance();
- tcp.bind({address: '0.0.0.0', port: 12121, family: 1}, err => {
- if (err) {
- console.log('bind fail');
- return;
- }
- console.log('bind success');
- })
- tcp.on('message', value => {
- console.log("on message, message:" + value.message + ", remoteInfo:" + value.remoteInfo)
- let da = resolveArrayBuffer(value.message);
- let dat_buff = String(da);
- //此处对接受到的数据进行处理
- });
- //将接受到的数据转化为文本型
- function resolveArrayBuffer(message){
- if (message instanceof ArrayBuffer) {
- let dataView = new DataView(message)
- let str = ""
- for (let i = 0;i < dataView.byteLength; ++i) {
- let c = String.fromCharCode(dataView.getUint8(i))
- if (c !== "n") {
- str += c
- }
- }
- return str;
- }
- }
- //数据的发送函数
- function send_once(Con_buff) {
- if (flag == false) {
- let promise = tcp.connect({ address: { address: 'xxx.xxx.xxx.xxx', port: xxxx, family: 1 }, timeout: 2000 });
- promise.then(() => {
- console.log('connect success');
- flag = true;
- tcp.send({
- data: Con_buff
- }, err => {
- if (err) {
- console.log('send fail');
- return;
- }
- console.log('send success');
- })
- }).catch(err => {
- console.log('connect fail');
- });
- } else if (flag == true) {
- tcp.send({
- data: Con_buff
- }, err => {
- if (err) {
- console.log('send fail');
- return;
- }
- console.log('send success');
- })
- }
- }
(3)界面设计OpenHarmony(eTs)界面设计(简单)教程:https://www.bilibili.com/video/BV1zV4y1H7fY本APP共用到了按钮、图片、标签三个部分,其对应的官网连接如下
竖向排列(Column):
https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-column.md/
其中强制位移(translate):
(4)参数动态更新- xxxxxxxxxx @State srtText: string = "测试变量"; Text(this.srtText) //动态 .fontSize(60) .fontWeight(FontWeight.Bold) .fontColor("#e94674") Button() { //按钮控件 Text('点击') .fontSize(50) .fontWeight(FontWeight.Bold) }.type(ButtonType.Capsule) .margin({ top: 200 }) .width('50%') .height('10%') .backgroundColor('#0D9FFB') .onClick(() => { //点击事件 this.srtText = "更改内容" //更改数据 })
在使用 @State变量对组件进行刷新时,发现只能在build中实现动态刷新,在外部创建全局变量或者外部函数的方式都不能实现,查阅资料后得到如下部分:
AppStorage与组件同步在管理组件拥有的状态中,已经定义了如何将组件的状态变量与父组件或祖先组件中的@State装饰的状态变量同步,主要包括@Prop、@Link、@Consume。
本章节定义如何将组件变量与AppStorage同步,主要提供@StorageLink和@StorageProp装饰器。
@StorageLink装饰器组件通过使用@StorageLink(key)装饰的状态变量,与AppStorage建立双向数据绑定,key为AppStorage中的属性键值。当创建包含@StorageLink的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。在UI组件中对@StorageLink的状态变量所做的更改将同步到AppStorage,并从AppStorage同步到任何其他绑定实例中,如PersistentStorage或其他绑定的UI组件。
@StorageProp装饰器组件通过使用@StorageProp(key)装饰的状态变量,将与AppStorage建立单向数据绑定,key标识AppStorage中的属性键值。当创建包含@StoageProp的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。AppStorage中的属性值的更改会导致绑定的UI组件进行状态更新。
- let varA = AppStorage.Link('varA')
- let envLang = AppStorage.Prop('languageCode')
- @Entry
- @Component
- struct ComponentA {
- @StorageLink('varA') varA: number = 2
- @StorageProp('languageCode') lang: string = 'en'
- private label: string = 'count'
- private aboutToAppear() {
- this.label = (this.lang === 'zh') ? '数' : 'Count'
- }
- build() {
- Row({ space: 20 }) {
- Button(`${this.label}: ${this.varA}`)
- .onClick(() => {
- AppStorage.Set('varA', AppStorage.Get('varA') + 1)
- })
- Button(`lang: ${this.lang}`)
- .onClick(() => {
- if (this.lang === 'zh') {
- AppStorage.Set('languageCode', 'en')
- } else {
- AppStorage.Set('languageCode', 'zh')
- }
- this.label = (this.lang === 'zh') ? '数' : 'Count'
- })
- }
- }
- }
即通过AppStorage.Link和 @StorageLink的方式,可实现外部动态刷新Text组件和image组件(等等之类都可以),方便我们在全局调用时更新数据。