玩嗨 OpenHarmony:基于 OpenHarmony 的家庭医生终端系统-体重体脂测量
原文引自51CTO 开源基础软件社区 #冲刺创作新星#《OpenHarmony-家庭医生终端系统-体重体脂测量》
项目介绍
项目名称:基于OpenHarmony的家庭医生终端系统-体重体脂测量(具有独立APP)项目实现功能:
- 采集被测人体体重
- 输入被测人体数据计算体脂
- 具有WEB配网功能
- 与服务器进行连接并实现数据交互
- 可使用清洁能源(太阳能板进行供电和充电)
- 已开发基于OpenHarmony(eTs)的控制APP
- 具有离线屏幕显示功能(OLED-0.96寸)
WEB配网
教程视频:https://www.bilibili.com/video/BV19L411M75o
1. 碰一碰配网介绍
通过一机一码的形式,识别到NFC后云端验证设备,进行弹窗拉起,再由NAN或AP的方式,实现发送配网的SSID和Password。
1.1 NAN配网
- 操作设备上配网键让设备进入配网模式
- 手机碰一碰设备上的NFC标签,拉起轻应用
- 选择配网wifi
- 调用 discoveryByNAN接口code为0
- 调用connectDevice接口连接设备
- 调用configDeviceNet接口开始配网
- 调用disconnectDevice接口断开网络
- 调用检测设备是否上线接口
- 检测到设备上线,调用绑定设备接口
1.2 AP配网
- 操作设备上配网键让设备进入配网模式
- 手机碰一碰设备上的NFC标签,拉起轻应用
- 选择配网wifi
- 调用discoveryByNAN接口code不为0
- 调用discoveryBySoftAp接口搜索当前设备的ap,搜索不到的话尝试直接去连接ap
- 调用connectDevice接口连接设备
- 调用configDeviceNet接口开始配网
- 调用disconnectDevice接口断开网络
- 调用检测设备是否上线接口
- 检测到设备上线,调用绑定设备接口
2. WEB配网
本章主要讲述如何实现web配网,是在STA模式下,模拟为一个网站服务器,当手机或其它设备进行访问时,检测是否为浏览器的协议头(HTTP),返回一个封装好的网页界面,通过网页上输入框的填写实现配网。
应用:编写基于http协议的数据传输程序(网站中浏览器端获取网页的过程)。
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>
实现的效果如下:
4. soft模式下实现网页服务器
该部分步骤分为四步:打开WIFI、进入softap模式,创建tcp服务器,解析HTTP指令。此处可参照润和开源项目:
https://gitee.com/hihopeorg/HarmonyOS-IoT-Application-Development/tree/master
4.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");
}
4.2 进入softap模式
在softap.c文件下WifiAPTask函数,注册回调
//注册wifi事件的回调函数
g_wifiEventHandler.OnHotspotStaJoin = OnHotspotStaJoinHandler;
g_wifiEventHandler.OnHotspotStaLeave = OnHotspotStaLeaveHandler;
g_wifiEventHandler.OnHotspotStateChanged = OnHotspotStateChangedHandler;
error = RegisterWifiEvent(&g_wifiEventHandler);
4.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
HI3861:鸿蒙网页显示传感器数据: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: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<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");
}
}
4. 体重测量(应变片+HX711驱动)
体重的核心传感器是使用的应变片:
核心原理是通过形变改变电阻的阻值,这个变化一般是线性关系,而通过阻值的关系可以计算出体重即物体的质量,很多商贩用的称重也是这种器件。
除了应变片之外我们还需要使用高精度的ADC,因为应变片形变引起的变化量是非常小的,必须使用高精度的ADC才能采集出来,而HI3861的自身ADC是10位即4096级别的,远不能满足我们的要求,所以选择了通用搭配的HX711来实现。
HX711是一款专为高精度电子秤而设计的24位A/D转换器芯片。与同类型其它芯片相比,该芯片集成了包括稳压电源、片内时钟振荡器等其它同类型芯片所需要的外围电路,具有集成度高、响应速度快、抗干扰性强等优点。降低了电子秤的整机成本,提高了整机的性能和可靠性。该芯片与后端MCU 芯片的接口和编程非常简单,所有控制信号由管脚驱动,无需对芯片内部的寄存器编程。输入选择开关可任意选取通道A 或通道B,与其内部的低噪声可编程放大器相连。通道A 的可编程增益为128 或64,对应的满额度差分输入信号幅值分别为±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);
}
5. 体脂计算
- 体脂%=1.2×BMI +0.23x年龄-5.4 -10.8×性别(其中男性性别取值为1,女性取值为0)
- 体重指数 BMI=体重/身高的平方(国际单位kg/m )
- 男正常体脂率约在10~20%之间,女约在17~30%之间。
APP开发
1. 环境搭建
使用的是官方下载地址: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/
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
本APP共用到了按钮、图片、标签三个部分,其对应的官网连接如下
- 按钮(Button):
- 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/
- 标签(TEXT):
- 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/
- 横向排列(Row):
- 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/
- 单选框(Radio):
- 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 = "更改内容" //更改数据
})
AppStorage与组件同步
在管理组件拥有的状态中,已经定义了如何将组件的状态变量与父组件或祖先组件中的@State装饰的状态变量同步,主要包括@Prop、@Link、@Consume。
本章节定义如何将组件变量与AppStorage同步,主要提供@StorageLink和@StorageProp装饰器。
1. @StorageLink装饰器
组件通过使用@StorageLink(key)装饰的状态变量,与AppStorage建立双向数据绑定,key为AppStorage中的属性键值。当创建包含@StorageLink的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。在UI组件中对@StorageLink的状态变量所做的更改将同步到AppStorage,并从AppStorage同步到任何其他绑定实例中,如PersistentStorage或其他绑定的UI组件。
2. @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<number>('varA', AppStorage.Get<number>('varA') + 1)
})
Button(`lang: ${this.lang}`)
.onClick(() => {
if (this.lang === 'zh') {
AppStorage.Set<string>('languageCode', 'en')
} else {
AppStorage.Set<string>('languageCode', 'zh')
}
this.label = (this.lang === 'zh') ? '数' : 'Count'
})
}
}
}
即通过AppStorage.Link和 @StorageLink的方式,可实现外部动态刷新Text组件和image组件(等等之类都可以),方便我们在全局调用时更新数据。
写在最后
我们最近正带着大家玩嗨OpenHarmony。如果你有好玩的东东,欢迎投稿,让我们一起嗨起来!有点子,有想法,有Demo,立刻联系我们:
合作邮箱:zzliang@atomsource.org