一、项目介绍
基于OpenHarmony使用HI3861实现空气质量的检测和上传(具有独立APP)
- 采集被测人体体重
- 输入被测人体数据计算体脂
- 具有WEB配网功能
- 与服务器进行连接并实现数据交互
- 可使用清洁能源(太阳能板进行供电和充电)
- 已开发基于OpenHarmony(eTs)的控制APP
- 具有离线屏幕显示功能(OLED-0.96寸)
二、WEB配网
教程视频:
(1)碰一碰配网介绍
通过一机一码的形式,识别到NFC后云端验证设备,进行弹窗拉起,再由NAN或AP的方式,实现发送配网的SSID和Password。
NAN配网
- 操作设备上配网键让设备进入配网模式
- 手机碰一碰设备上的NFC标签,拉起轻应用
- 选择配网wifi
- 调用 discoveryByNAN接口code为0
- 调用connectDevice接口连接设备
- 调用configDeviceNet接口开始配网
- 调用disconnectDevice接口断开网络
- 调用检测设备是否上线接口
- 检测到设备上线,调用绑定设备接口
AP配网
- 操作设备上配网键让设备进入配网模式
- 手机碰一碰设备上的NFC标签,拉起轻应用
- 选择配网wifi
- 调用discoveryByNAN接口code不为0
- 调用discoveryBySoftAp接口搜索当前设备的ap,搜索不到的话尝试直接去连接ap
- 调用connectDevice接口连接设备
- 调用configDeviceNet接口开始配网
- 调用disconnectDevice接口断开网络
- 调用检测设备是否上线接口
- 检测到设备上线,调用绑定设备接口
(2)WEB配网
本章主要讲述如何实现web配网,是在STA模式下,模拟为一个网站服务器,当手机或其它设备进行访问时,检测是否为浏览器的协议头(HTTP),返回一个封装好的网页界面,通过网页上输入框的填写实现配网。
HTTP协议介绍:
- 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>
```### 实现的效果如下:
![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");
}
#
在softap.c文件下WifiAPTask函数,注册回调
//注册wifi事件的回调函数
g_wifiEventHandler.OnHotspotStaJoin = OnHotspotStaJoinHandler;
g_wifiEventHandler.OnHotspotStaLeave = OnHotspotStaLeaveHandler;
g_wifiEventHandler.OnHotspotStateChanged = OnHotspotStateChangedHandler;
error = RegisterWifiEvent(&g_wifiEventHandler);
#
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));
}
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);
}
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);
#
#
#
#
#
**在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:063
//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;
}
}
}
}
**通过该函数,就能实现传感器数值和字符的显示。**
#
**因为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是一款专为高精度电子秤而设计的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);
}
### (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');
})
}
}
**本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/)
@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:
## AppStorage与组件同步
**在**[管理组件拥有的状态](https:
**本章节定义如何将组件变量与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组件(等等之类都可以),方便我们在全局调用时更新数据。**