玩嗨 OpenHarmony:基于 OpenHarmony 的家庭医生终端系统-血压/心率/血氧检

玩嗨 OpenHarmony:基于 OpenHarmony 的家庭医生终端系统-血压/心率/血氧检

原文引自51CTO 开源基础软件社区 #冲刺创作新星#《OpenHarmony-家庭医生终端系统-血压/心率/血氧检》

项目介绍

项目名称:基于OpenHarmony使用HI3861实现血压、心率、血氧的检测和上传(具有独立APP)项目实现功能:

  • 采集被测人体血压(高血压/低血压参数)
  • 采集被测人体心率参数
  • 采集被测人体血氧参数
  • 具有WEB配网功能
  • 与服务器进行连接并实现数据交互
  • 可使用清洁能源(太阳能板进行供电和充电)
  • 开发基于OpenHarmony的控制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. 血压测量驱动

血压的测量选用惊帆科技的JFH141,产品外观如下:

4.1 产品优势
  • 脉搏波形、心率值、血氧值和血管微循环参数可直接输出
  • 一体化集成红光红外光 绿光三光源LED可用于血氧测量
  • 宽光谱高灵敏度的光传感器
  • 11.18mm*8.13mm超小体积
  • 超低工作功耗
  • 2.7V~3.3V灵活的电平接口
  • 易于使用的UART接口输出
  • 对接大数据数据分析平台
4.2 产品规格
型号     JFH141
长度     11.18mm
宽度     8.13mm
工作电压  2.6-3.6V
工作电流  5mA
休眠电流  3.4uA
输出数据  心率 血氧 微循环
应用场景  血氧手环 指环 饰品 血氧仪 
4.3 USART驱动协议

通过对数据的截取和发送即可实现。

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 = "更改内容"      //更改数据
      })

在使用 @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装饰器。

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