OpenHarmony开源社区
直播中

韩乐

6年用户 120经验值
擅长:嵌入式技术,处理器/DSP,控制/MCU
私信 关注
[经验]

OpenHarmony-家庭医生终端系统-体重体脂测量

一、项目介绍

基于OpenHarmony使用HI3861实现空气质量的检测和上传(具有独立APP)

  • 采集被测人体体重
  • 输入被测人体数据计算体脂
  • 具有WEB配网功能
  • 与服务器进行连接并实现数据交互
  • 可使用清洁能源(太阳能板进行供电和充电)
  • 已开发基于OpenHarmony(eTs)的控制APP
  • 具有离线屏幕显示功能(OLED-0.96寸)
    3.jpg

二、WEB配网

教程视频:

(1)碰一碰配网介绍

11.png
通过一机一码的形式,识别到NFC后云端验证设备,进行弹窗拉起,再由NAN或AP的方式,实现发送配网的SSID和Password。

NAN配网

  1. 操作设备上配网键让设备进入配网模式
  2. 手机碰一碰设备上的NFC标签,拉起轻应用
  3. 选择配网wifi
  4. 调用 discoveryByNAN接口code为0
  5. 调用connectDevice接口连接设备
  6. 调用configDeviceNet接口开始配网
  7. 调用disconnectDevice接口断开网络
  8. 调用检测设备是否上线接口
  9. 检测到设备上线,调用绑定设备接口

AP配网

  1. 操作设备上配网键让设备进入配网模式
  2. 手机碰一碰设备上的NFC标签,拉起轻应用
  3. 选择配网wifi
  4. 调用discoveryByNAN接口code不为0
  5. 调用discoveryBySoftAp接口搜索当前设备的ap,搜索不到的话尝试直接去连接ap
  6. 调用connectDevice接口连接设备
  7. 调用configDeviceNet接口开始配网
  8. 调用disconnectDevice接口断开网络
  9. 调用检测设备是否上线接口
  10. 检测到设备上线,调用绑定设备接口

(2)WEB配网

本章主要讲述如何实现web配网,是在STA模式下,模拟为一个网站服务器,当手机或其它设备进行访问时,检测是否为浏览器的协议头(HTTP),返回一个封装好的网页界面,通过网页上输入框的填写实现配网。
12.png

HTTP协议介绍:

  1. http协议->超文本传输协议
  2. 应用:编写基于http协议的数据传输程序(网站中浏览器端获取网页的过程)
  3. http请求作用:将要获取的内容以http协议的格式发送给服务端,服务端根据格式进行解析获取到其真实内容,将结果以http协议的格式回复给客户端

(3)WEB配网界面

html源代码如下

<html><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>程皖配网</title></head><body><form name="my"><div align="center"><font size="16">欢迎使用程皖配网</font> </div><br><div align="center">WiFi名称:<input type="text" name="s" placeholder="请输入您WiFi的名称" id="aa" style="text-align:center"></div><br><div align="center">WiFi密码:<input type="text" name="p" placeholder="请输入您WiFi的密码" id="bb"></div><br><div align="center">服务器IP:<input type="text" name="i" placeholder="请输入您的服务器IP" id="cc"></div><br><div align="center">服务器端口:<input type="text" name="t" placeholder="请输入您的服务器端口" id="dd"></div> <br><div align="center"><input type="button" value="连接" onclick="wifi()" style="width:150px;height:40px"  ></div></form><script language="javascript">function wifi(){var ssid = my.s.value;var password =my.p.value;var tcp_ip = my.i.value;var tcp_port = my.t.value;var xmlhttp=new XMLHttpRequest();xmlhttp.open("GET","/HandleVal?ssid="+ssid+"&password="+password+"&tcp_ip="+tcp_ip+"&tcp_port="+tcp_port,true);xmlhttp.send()}</script></body></html>
```### 实现的效果如下:

![14.png](//file.elecfans.com/web2/M00/66/7E/poYBAGMQnGWACyJyAAAzrAmWxAw014.png)

### (4)soft模式下实现网页服务器

**该部分步骤分为四步:打开WIFI、进入softap模式,创建tcp服务器,解析HTTP指令。此处可参照**

**润和开源项目:**

[https://gitee.com/hihopeorg/HarmonyOS-IoT-Application-Development/tree/master](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 \r\n");
}else
{
//printf("recv :%s\r\n", recvbuf);
//返回s1中包含s2所有字符的最大起始段长度
//size_t strspn(const char *s1, const char s2);
char
p= strstr(recvbuf,TEST);
uint16_t DIR_buff = p - recvbuf;
printf("\r\nThe GET HTTP num:%d\r\n",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<p2)
{
p1 += strlen("ssid=");
memcpy(get_ssid, p1, p2 - p1);
printf("\r\nget the ssid = %s\r\n", get_ssid);
}
p1 = strstr(recvbuf, "password=");
p2 = strstr(recvbuf, "&tcp_ip");
if(p1!=0 && p2!=0 && p1<p2)
{
p1 += strlen("password=");
memcpy(get_pwd, p1, p2 - p1);
printf("get the ssid = %s\r\n", 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<p2)
{
p1 += strlen("ssid=");
memcpy(get_ssid, p1, p2 - p1);
printf("\r\nget the ssid = %s\r\n", get_ssid);
}


**此时得到帐号密码后尝试连接,即实现网页配网**

WifiConnect(get_ssid,get_pwd);


## 三、外设驱动

### 本系统使用到usart(PM2.5传感器)、IIC(OLED显示屏)、单总线(DHT11)三个部分和TCP(双线程收发)几个部分

### Winodows下HI3861开发:[https://www.bilibili.com/video/BV1PY41147z8](https://www.bilibili.com/video/BV1PY41147z8)

### HI3861:鸿蒙网页显示传感器数据:[https://www.bilibili.com/video/BV1L34y1k7im](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 error\n");
}else
{
printf("IIC sucesefful\n");
}
OLED_ColorTurn(0);//0正常显示,1 反色显示
OLED_DisplayTurn(0);//0正常显示 1 屏幕翻转显示


**其中主要用到的函数是void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size1):**


//在指定位置显示一个字符,包括部分字符
//x:0127
//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<size2;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)数据发送和接收

**因为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!\r\n");
goto do_cleanup;
}

// 尝试和目标主机建立连接,连接成功会返回0 ,失败返回 -1
if (connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
printf("connect failed!\r\n");
goto do_cleanup;
}
printf("connect to server %s success!\r\n", host);
Wifi_SOCKET_GET();

while (1)
{
osDelay(500);
/////////////////////////////////////////////////////////上传函数
retval = send(sockfd, buff, 6,0);//其中buff为数据
}

do_cleanup:
printf("do_cleanup...\r\n");
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!\r\n");//此处对接收到的数据进行处理,并执行对应内容
}
}
}
do_cleanup:
printf("do_cleanup...\r\n");
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!\r\n");
}
}


### (3)体重测量(应变片+HX711驱动)

**体重的核心传感器是使用的应变片**
![26.png](//file.elecfans.com/web2/M00/6C/61/pYYBAGMtZUCAMIUTAAb8CTwzUVM782.png)

**核心原理是通过形变改变电阻的阻值,这个变化一般是线性关系,而通过阻值的关系可以计算出体重即物体的质量,很多商贩用的称重也是这种器件。**

**除了应变片之外我们还需要使用高精度的ADC,因为应变片形变引起的变化量是非常小的,必须使用高精度的ADC才能采集出来,而HI3861的自身ADC是10位即4096级别的,远不能满足我们的要求,所以选择了通用搭配的HX711来实现。**
**HX711是一款专为高精度电子秤而设计的24A/D转换器芯片。与同类型其它芯片相比,该芯片集成了包括稳压电源、片内时钟振荡器等其它同类型芯片所需要的外围威廉希尔官方网站
,具有集成度高、响应速度快、抗干扰性强等优点。降低了电子秤的整机成本,提高了整机的性能和可靠性。该芯片与后端MCU 芯片的接口和编程非常简单,所有控制信号由管脚驱动,无需对芯片内部的寄存器编程。输入选择开关可任意选取通道A 或通道B,与其内部的低噪声可编程放大器相连。通道A 的可编程增益为12864,对应的满额度差分输入信号幅值分别为±20mV或±40mV。通道B 则为固定的32 增益,用于系统参数检测。芯片内提供的稳压电源可以直接向外部传感器和芯片内的A/D 转换器提供电源,系统板上无需另外的模拟电源。**

**通过描述可以得知HX711是使用SCL+DO的形式实现的,即一根为数据线一根为信号线,在HI3861的驱动程序如下:**

unsigned long ReadCount()
{
unsigned long Count;
unsigned char i;
hi_gpio_value gpio_val;
hi_gpio_set_ouput_val(HI_IO_NAME_GPIO_7,1);
hi_gpio_set_ouput_val(HI_IO_NAME_GPIO_8,0);
//hi_gpio_set_ouput_val(HI_IO_NAME_GPIO_7,0);

hi_gpio_set_dir(HI_GPIO_IDX_7, HI_GPIO_DIR_IN);
hi_gpio_get_input_val(HI_GPIO_IDX_7, &gpio_val);
while(gpio_val)
{
hi_gpio_get_input_val(HI_GPIO_IDX_7, &gpio_val);
}

for (i=0;i<24;i++)
{
hi_gpio_set_ouput_val(HI_IO_NAME_GPIO_8,1);
Count = Count<<1; //下降沿来时变量Count左移一位,右侧补零
//osDelay(1);
hi_gpio_set_ouput_val(HI_IO_NAME_GPIO_8,0);
hi_gpio_get_input_val(HI_GPIO_IDX_7, &gpio_val);
if(gpio_val)
{
Count++;
}
}
hi_gpio_set_ouput_val(HI_IO_NAME_GPIO_8,1);
Count=Count^0x800000;//第25个脉冲下降沿来时,转换数据
//osDelay(1);
hi_gpio_set_ouput_val(HI_IO_NAME_GPIO_8,0);
return(Count);
}


### (4)体脂计算

* **体脂%=1.2×BMI +0.23x年龄-5.4 -10.8×性别(其中男性性别取值为1,女性取值为0)**
* **体重指数 BMI=体重/身高的平方(国际单位kg/m )**
* **男正常体脂率约在10~20%之间,女约在17~30%之间。**

## 四、APP开发

### (1)环境搭建

![25.png](//file.elecfans.com/web2/M00/6C/61/pYYBAGMtZZWAUnBPAAVk08kK9I8625.png)

**使用的是官方下载地址:**[https://developer.harmonyos.com/cn/develop/deveco-studio#download_beta](https://developer.harmonyos.com/cn/develop/deveco-studio#download_beta)

### 2)TCP数据交互

**该部分参考官方手册:**[https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/apis/js-apis-socket.md/](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/apis/js-apis-socket.md/)

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界面设计(简单)教程:[https://www.bilibili.com/video/BV1zV4y1H7fY](https://www.bilibili.com/video/BV1zV4y1H7fY)

**本APP共用到了按钮、图片、标签三个部分,其对应的官网连接如下**

**按钮(Button):**[https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-button.md/](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-button.md/)

**图片(Image):**[https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-image.md/](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-image.md/)

**标签(TEXT):**[https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-text.md/](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-text.md/)

**竖向排列(Column):**

[https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-column.md/](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-column.md/)

**横向排列(Row):**[https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-row.md/](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-row.md/)

**输入框(TextInput):**[https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-textinput.md/](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-textinput.md/)

**单选框(Radio):**[https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-radio.md/](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-radio.md/)

### (4)参数动态更新

@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中实现动态刷新,在外部创建全局变量或者外部函数的方式都不能实现,查阅资料后得到如下部分:**

**官方文档:**[https://docs.openharmony.cn/pages/v3.1/zh-cn/application-dev/ui/ts-application-states-appstorage.md/](https://docs.openharmony.cn/pages/v3.1/zh-cn/application-dev/ui/ts-application-states-appstorage.md/)

## AppStorage与组件同步

**在**[管理组件拥有的状态](https://docs.openharmony.cn/pages/v3.1/zh-cn/application-dev/ui/ts-component-states-state.md/)中,已经定义了如何将组件的状态变量与父组件或祖先组件中的@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组件(等等之类都可以),方便我们在全局调用时更新数据。**

更多回帖

发帖
×
20
完善资料,
赚取积分