一、项目介绍
基于OpenHarmony使用HI3861实现水质的检测和上传(具有独立APP)
- 采集水质TDS
- 具有WEB配网功能
- 与服务器进行连接并实现数据交互
- 可使用清洁能源(太阳能板进行供电和充电)
- 开发基于OpenHarmony的控制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>
实现的效果如下:
(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函数,注册回调
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
{
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);
}
在这个循环中实现了判断当前是否为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开发:
HI3861:鸿蒙网页显示传感器数据:
(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);
OLED_DisplayTurn(0);
其中主要用到的函数是void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size1):
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++)
{
if(size1==12)
{temp=asc2_1206[chr1][i];}
else if(size1==16)
{temp=asc2_1608[chr1][i];}
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)水质测量驱动(ADC)
水质的测量选择使用TDS传感器,该传感器为ADC类型的传感器,需要采集传感器的线性电压,调用HI3861的ADC硬件。
四、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界面设计(简单)教程:
本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/
(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/
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<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组件(等等之类都可以),方便我们在全局调用时更新数据。