1

#打卡不停更#家庭健康管理平台

 1 year ago
source link: https://blog.51cto.com/harmonyos/5725725
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

0. 项目简介

身体健康是一切生产生活的硬性基础。健康是福,一切安好,未来才可期。为什么经常跑步体重缺还在往上飘?突发紧急情况怎么处理?在数字时代,如何更好的为人们提供健康福祉、普及健康知识?如何进一步驱动个人健康管理是的值得研究的方向。为此,我们团队打造了一个健康管理平台——家庭健康管理平台。概览如下图所示:

家庭健康助理是集健康数据测量与管理、急救设备及使用指导、疫情防控实况、日常生活建议为一体的健康管理平台。健康助理核心是以数据为驱动,软硬结合,通过若干生理数据采集设备获取家庭成员健康状态,并将数据同步至移动端(DAYU200),软件进行数据管理与可视化,为每个成员打造健康日历,为每一个家庭提供数字化健康服务。 为展示平台的核心能力,开发的具体内容包含有:基于OpenHarmony驱动的移动端软件、多功能生理数据测量笔、便携式止血设备、康复训练手套。 交互软件完成了设备管理、数据可视化(分布式数据存储)、急救知识展示功能;多功能生理数据测量笔展示平台数据测量能力,测量包括体温、身高、心率血氧;便携式止血设备是为展示平台急救设备及使用指导能力,提供止血教学、实时查看止血状态服务;康复训练手套是为患者提供手部康复训练服务。

对应的联合国17项可持续发展目标

3.良好健康与福祉、 4.优质教育

1.开发环境及技术特点

OH系统版本:OpenHarmony 3.1 Release
开发环境:DevEco studio 3.0.0.900
技术特点: ArkUI (ets) 、软硬结合、http数据请求、加载web组件、socket通信、健康数据对象、分布式数据管理(开发中)
基础开发指导: 环境准备 设备开发指导 应用开发指导

2.开发流程

2.1 家庭健康管理平台框架

如下图所示,家庭健康管理平台由家庭助理APP、分布式设备组成。APP核心部分是数据采集、医疗相关知识模块、健康数据管理三部分。

  • 数据采集使用各种医疗设备进行采集,本项目中以多功能测量笔为例,展示数据采集、自动同步的功能,免去复杂的手动输入数据操作。
  • 针对医疗相关知识模块,开发了便携式止血设备,并对肢体止血进行指导。
  • 健康数据管理负责整合各项数据,如天气、疫情信息、成员体温等。在成员信息页展示成员档案。还可利用分布式特性将数据传输至另一台设备,实现健康数据共享,方便管理家庭成员数据,更好的服务家庭。(涉及轻量数据存储,在开发中)

注: 对应的联合国17项可持续发展目标 3.良好健康与福祉 4.优质教育
APP 主页:

下面以APP页面为主线,对项目开发技术细节进行展开说明。

2.2 家庭成员健康档案页

2.2.1 数据抽象

将成员健康档案抽象为一个类,包含年龄、身高、心率等数据。另外定义对象操作方法,方便访问数据,新建对象。实现代码如下:

export class People {
  name: string
  age: number
  height: number
  weight: number
  heart: number
  temperature: number

  constructor(name:string, age:number, height:number, weight:number, heart:number, temperature:number){
    this.name = name
    this.age  = age
    this.height = height
    this.weight = weight
    this.heart = heart
    this.temperature = temperature
  }
}

export const PersonMsg: any[]=[
  {'name': '爷爷', 'age': 1,'height': 190, 'weight': 10, 'heart': 1,'temperature': 1},
  {'name': '奶奶', 'age': 2,'height': 180, 'weight': 20, 'heart': 2,'temperature': 2},
  {'name': '爸爸', 'age': 3,'height': 170, 'weight': 30, 'heart': 3,'temperature': 3},
  {'name': '妈妈', 'age': 4,'height': 160, 'weight': 40, 'heart': 4,'temperature': 4},
  {'name': '小可爱', 'age': 5,'height': 150,'weight': 60, 'heart': 5,'temperature': 5},
];
export function PersonInit(): Array<People> {
  let PersonDataArray: Array<People> = []
  PersonMsg.forEach(item => {
    PersonDataArray.push(new People(item.name, item.age, item.height, item.weight, item.heart, item.temperature));
  })
  return PersonDataArray;
}

2.2.2 展示信息

使用SideBar组件,迭代读取数据,对每一位成员的健康档案进行展示,并自动计算BMI。

import {People,PersonInit} from '../common/mystorage'
// 使用SideBar组件展示
SideBarContainer(SideBarContainerType.Embed)
    {
      Column() {
        ForEach(this.arr, (item, index) => {
          Column({ space: 5 }) {
            Image(this.current === item ? this.selectedIcon : this.normalIcon).width(64).height(64)
            Text(this.personList[item-1].name )
              .fontSize(25)
              .fontColor(this.current === item ? '#0A59F7' : '#999')
              .fontFamily('source-sans-pro,cursive,sans-serif')
          }
          .onClick(() => {
            this.current = item
            putStorage()
            this.BMI = this.person_arr[this.current-1].weight/Math.pow(this.person_arr[this.current-1].height/100,2)
            if(this.BMI<18.5){
              this.bodyStatus = '偏瘦'
            }
            else if(this.BMI>24.9){
              this.bodyStatus = '月半'
            }
            else{
              this.bodyStatus = '正常'
            }
          })
        }, item => item)
      }.width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .backgroundColor('#19000090')
      Column(){
        Row({space:20}) {
          Image($r('app.media.ic_contacts_business_cards')).width('50%').height('20%').objectFit(ImageFit.Contain)
            .onClick(() => {
              this.personList[this.current].name = 'A'
              this.person_arr[this.current].name = 'B'
              putStorage()
              prompt.showToast({ message: this.personList[this.current].name })
            })
          Text(this.person_arr[this.current-1].name).fontSize(40)
        }
          Row() {
            Image($r('app.media.ic_contacts_birthday_filled')).objectFit(ImageFit.Contain)
              .size({ height: 80, width: 80 })
            Text('年龄: ' + this.person_arr[this.current-1].age).fontSize(this.info_font_size).margin({ top: 10 })
          }.margin({top:10})
          Row(){
            Image($r('app.media.ic_user_portrait')).objectFit(ImageFit.Contain)
              .size({ height: 80, width: 80 })
            Text('身高: ' + this.person_arr[this.current-1].height).fontSize(this.info_font_size)
          }.margin({top:10})
          Row(){
            Image($r('app.media.ic_public_privacy')).objectFit(ImageFit.Contain)
              .size({ height: 80, width: 80 })
            Text('体重: ' + this.person_arr[this.current-1].weight.toString()).fontSize(this.info_font_size)
          }.margin({top:10})
          Row() {
            Image($r('app.media.ic_contacts_blood_type')).objectFit(ImageFit.Contain)
              .size({ height: 80, width: 80 })
            Text('心率: ' + this.person_arr[this.current-1].heart.toString())
              .fontSize(this.info_font_size)
              .margin({ top: 10 })
          }.margin({top:10})
          Row() {
            Image($r('app.media.ic_controlcenter_eyeconfort_filled')).objectFit(ImageFit.Contain)
              .size({ height: 80, width: 80 })
            Text('体温: ' + this.person_arr[this.current-1].temperature).fontSize(this.info_font_size).margin({ top: 10 })
          }.margin({top:10})
          TextInput({ placeholder: 'BMI指数:  ' + this.BMI.toString().substring(0, 4) + ' '+ this.bodyStatus })
            .width('90%').height('5%').fontSize(30)
            .placeholderFont({ size: this.info_font_size, weight: 100, family: 'cursive' })
      }.width('100%').height('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Start).margin({left:30})
    }
    .sideBarWidth(240)
    .minSideBarWidth(209)
    .maxSideBarWidth(260)
    .onChange((value: boolean) => {
      console.info('status:' + value)
    })

展示的数据涉及分布式轻量化数据存储,在另外一台设备可查看成员信息。
完整的数据库开发流程封装如下:

// 导入模块
import dataStorage from '@ohos.data.storage';
import prompt from '@ohos.prompt';
import distributedData from '@ohos.data.distributedData';
import deviceManager from '@ohos.distributedHardware.deviceManager';

let kvManager;
let kvStore;
let devManager;
// 订阅分布式数据库管理对象
export function CreateKVManger()
{

  try {
    const kvManagerConfig = {
      bundleName : 'com.example.familyheath',
      userInfo : {
        userId : '0',
        userType : distributedData.UserType.SAME_USER_ID
      }
    }
    distributedData.createKVManager(kvManagerConfig, function (err, manager) {
      if (err) {
        console.log("createKVManager err: "  + JSON.stringify(err));
        return;
      }
      console.log("cccc createKVManager success");
      kvManager = manager;
    });
  } catch (e) {
    console.log("cccc An unexpected error occurred. Error:" + e);
  }
}
// 创建分布式数据库
export function CreateKVStore()
{
  try {
    const options = {
      createIfMissing : true,
      encrypt : false,
      backup : false,
      autoSync : false,
      kvStoreType : distributedData.KVStoreType.SINGLE_VERSION,
      securityLevel : distributedData.SecurityLevel.S0,
    };
    kvManager.getKVStore('storeId10087', options, function (err, store) {
      if (err) {
        console.log("xxxxx getKVStore err: "  + JSON.stringify(err));
        return;
      }
      console.log("cccc getKVStore success");
      kvStore = store;
    });
  } catch (e) {
    console.log("cccc An unexpected error occurred. Error:" + e);
  }
}

// 订阅分布式数据库
export function OnKVStore()
{
  kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_ALL, function (data) {
    console.log("dataChange callback call data: " + JSON.stringify(data));
  });
}

// 插入kv数据库
export function InsertKVStore(key:string,value)
{
  const KEY_STRING_ELEMENT = key;
  const VALUE_STRING_ELEMENT = value;
  try {
    kvStore.put(KEY_STRING_ELEMENT, VALUE_STRING_ELEMENT, function (err,data) {
      if (err != undefined) {
        console.log("cccc put err: " + JSON.stringify(err));
        return;
      }
      console.log("cccc put success");
    });
  }catch (e) {
    console.log("cccc An unexpected error occurred. Error:" + e);
  }

}
// 查找数据库
export function  SearchKVStore(key:string)
{
  let value = "cc"
  const KEY_STRING_ELEMENT = key;
  try {
    kvStore.get(KEY_STRING_ELEMENT, function (err,data) {
      console.log("cccc get success data: " + data);
      value = data
      return  value
    });
  }catch (e) {
    console.log("cccc An unexpected error occurred. Error:" + e);
  }
  return  value
}

// 同步分布式数据库到可信任设备
export function AsyncKVStore()
{
  // create deviceManager
  deviceManager.createDeviceManager("com.example.familyheath", (err, value) => {
    if (!err) {
      devManager = value;
      // get deviceIds
      let deviceIds = [];
      if (devManager != null) {
        // 获取分布式组网内可信设备
        var devices = devManager.getTrustedDeviceListSync();
        for (var i = 0; i < devices.length; i++) {
          deviceIds[i] = devices[i].deviceId;
        }
      }
      try{
        kvStore.sync(deviceIds, distributedData.SyncMode.PUSH_PULL, 1000);
      }catch (e) {
        console.log("cccc An unexpected error occurred. Error:" + e);
      }
    }
  });
}

2.3 数据采集模块

2.3.1 设备管理

数据采集模块负责管理家用医疗设备、健身器械,数据采集。理论上可接入如体脂秤、血压血糖仪、跑步机等常用设备。针对该模块,项目开发了多功能测试笔,2.3.2节进行介绍。设备管理页面框架如下:

单台设备的信息展示实现如下:

 // 测量笔
      Row({space:20}) {
        Column()
        {
          Text(this.pen).height('30%').fontSize(35)
          Image($r('app.media.multipen'))
            .width('45%').height('50%').margin({top:20})
            .objectFit(ImageFit.Contain)
        }.backgroundColor('white').borderRadius(20).opacity(this.pen_opacity)
        .onClick(() => {
          this.penShow = !this.penShow
          if(this.penShow){
            this.pen_opacity = 0.4
          }else{
            this.pen_opacity = 0.9
          }
          router.push({ url: 'pages/multiPen' })
        })

2.3.2 多功能测量笔-软件开发

  • 涉及模块、权限:
import socket from '@ohos.net.socket'
import prompt from '@ohos.prompt';
import wifi from '@ohos.wifi';
"ohos.permission.INTERNET"
"ohos.permission.GET_WIFI_INFO"

当在设备列表页面点击对应设备后,将跳转到对应的设备控制页面。项目以多功能数据测量笔为例,进行开发细节介绍。首先每台设备均使用socket与DAYU200建立连接,为了提升代码重用性,新建common文件夹,编写socket功能模块,外部只需进行简单调用即可。socket功能封装具体的实现如下:

import socket from '@ohos.net.socket'
import prompt from '@ohos.prompt';

//tcp对象
let tcp = socket.constructTCPSocketInstance();

let message_recv = '0'
  // 解析本地ip
export function  resolveIP(ip) {
    if (ip < 0 || ip > 0xFFFFFFFF) {
      throw ("The number is not normal!");
    }
    return (ip >>> 24) + "." + (ip >> 16 & 0xFF) + "." + (ip >> 8 & 0xFF) + "." + (ip & 0xFF);
  }

export function  tcpInit(localAddr) {
    tcp.on('connect', () => {
      let tcp_status = '连接ok'
      prompt.showToast({message:tcp_status})
    });
    tcp.on('message', value => {
      message_recv = resolveArrayBuffer(value.message)
      prompt.showToast({message:message_recv})
     return message_recv
    });
    tcp.on('close', () => {
      prompt.showToast({message:"tcp close"})
    });
    tcp.bind({ address: localAddr.address, port:localAddr.port, family: 1 })
      .then(() => {
        prompt.showToast({message:"bind tcp success",})
      }).catch(err => {
      prompt.showToast({message:"bind tcp failed"})
      return
    });
  }

export function tcpRecv() {
  tcp.on('message', value => {
    message_recv = resolveArrayBuffer(value.message)
    prompt.showToast({message:message_recv})
  });
  return message_recv
}

export function tcpConnect(localAddr,targetAddr) {
    tcp.getState()
      .then((data) => {
        if (data.isClose) {
          tcpInit(localAddr)
        }
        // 连接
        tcp.connect(
          {
            address: { address: targetAddr.address, port: targetAddr.port, family: 1 }, timeout: 2000
          }
        ).then(() => {
          prompt.showToast({message:" tcp connect successful"})
        }).catch((error) => {
          prompt.showToast({message:"tcp connect failed"})
        })
      })
  }

export function tcpSend(msg:string) {
    tcp.getState().then((data) => {
      if (data.isConnected) {
        // 发送消息
        tcp.send(
          { data: msg, }
        ).then(() => {
          prompt.showToast({message: msg+" send message successful"})
        }).catch((error) => {
          prompt.showToast({message:"send failed"})
        })
      } else {
        prompt.showToast({message:"tcp not connect"})
      }
    })
  }

 export function tcpClose() {
    tcp.close().then(() => {
      prompt.showToast({message:'断开TCP'})
    }).catch((err) => {
      prompt.showToast({message:"tcp 断开失败"})
    })
    tcp.off('close');
    tcp.off('message');
    tcp.off('connect');
  }

  // 解析ArrayBuffer
  export function resolveArrayBuffer(message: ArrayBuffer): string {
    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;
    }
  }

完成封装后即可在设备页面进行调用,设备页面使用统一模板,设计简约直观,多功能测量笔实现代码如下:

// 导入 socket封装模块
import {resolveIP,tcpInit, tcpConnect, tcpSend, tcpRecv, tcpClose, resolveArrayBuffer} from '../common/setting'

// 向笔请求数据
enum pullRequest {
  HEIGHT = 'h',
  TEMPERATURE = 'w' ,
  HEART = 'x',
  POWER = 'b',
};

@Entry
@Component
struct MultiPen {
  @State message: string = '多功能测量笔'
  @State height: string = '160'
  @State temperature: string = '36.0'
  @State heartSop: string = '88'
  @State heartCount: string = '102'
  private select: number = 1
  private people: string[] = ['爷爷', '奶奶', '爸爸', '妈妈','小可爱']
  @State battery: string = '89'
  @State InputIP: string = '192.168.43.149'
  @State recvMsg: string = '00000000'

  @State aboutPen: boolean = false
  @State inputShow:boolean = false

  // 本地ip
  localAddr = {
    address: resolveIP(wifi.getIpInfo().ipAddress),
    port:  8888
  };

  // 目标ip
   targetAddr = {
    address: this.InputIP,
    family: 1,
    port: 8888
  }

  build() {
      Column() {
        // 标题与返回按钮
        Row(){
          Image($r('app.media.ic_public_back'))
            .width('15%').height('5%').margin({top:20})
            .objectFit(ImageFit.Contain)
            .onClick(()=>{
             router.push({url:'pages/healthDevices'})
            })
          Text(this.message)
            .fontSize(60).fontWeight(FontWeight.Bold)
            .margin({top:10}).padding({left:20})
        }.borderRadius(20).width('90%').align(Alignment.Start)
        Divider().height('2%')

        // 产品展示- 多功能测量笔、使用说明
        Row()
        {
          if(this.aboutPen){
            Image($r('app.media.coverpen')).objectFit(ImageFit.Contain)
          }
          else{
            Image($r('app.media.multipen')).objectFit(ImageFit.Contain)
          }
        }.height('30%')

        Text('HealthPen')
          .fontSize(41)
          .fontWeight(FontWeight.Bold)

        // 开关、测量对象、电量、IP设置
        Row({space:40})
        {
          Image($r('app.media.ic_power_on')).width('10%').objectFit(ImageFit.Contain).margin({left:20})
          .onClick(()=>{
            tcpConnect(this.localAddr,this.targetAddr)
            this.recvMsg = tcpRecv()
          })
          Text('选择成员').width('5%')
          TextPicker({range: this.people, selected: this.select}).width('20%').height('100%')
            .onChange((value: string, index: number) => {
              console.info('Picker item changed, value: ' + value + ', index: ' + index)
            })
          Image($r('app.media.ic_statusbar_battery_powersaving')).width('18%').objectFit(ImageFit.Contain)
            .onClick(()=>{
              tcpConnect(this.localAddr,this.targetAddr)
              tcpSend(pullRequest.HEIGHT)
              this.recvMsg = tcpRecv()
              this.battery  = tcpRecv()
            })
            .overlay(this.battery, { align: Alignment.Center})
          Image($r('app.media.ic_public_settings_filled')).width('10%').objectFit(ImageFit.Contain).margin({left:10})
          .onClick(()=>
          {
            this.inputShow = !this.inputShow
          })

        }.height('15%').borderRadius(20).backgroundColor('#F5F5F5').width('90%').margin({top:10}).justifyContent(FlexAlign.Center)

        // 身高、体温
        Row({space:20})
        {
          Column()
          {
            Row({space:20})
            {
              Image($r('app.media.ic_user_portrait')).height('100%').width('20%').objectFit(ImageFit.Contain)
              Text('身高').fontSize(40)
            }.height('40%').justifyContent(FlexAlign.Start).width('100%')
            Text(this.height+' cm').fontSize(50).fontWeight(FontWeight.Bold)
          }.borderRadius(20).backgroundColor('#F5FFFA').width('48%')
          .onClick(()=>{
            tcpConnect(this.localAddr,this.targetAddr)
            tcpSend(pullRequest.HEIGHT)
            this.recvMsg = tcpRecv()
            this.height  = tcpRecv().substring(0,3)
          })
          Column()
          {
            Row({space:20})
            {
              Image($r('app.media.ic_user_portrait_select')).height('100%').width('20%').objectFit(ImageFit.Contain)
              Text('体温').fontSize(40)
            }.height('40%').justifyContent(FlexAlign.Start).width('100%')
            Text(this.temperature+' ℃').fontSize(50).fontWeight(FontWeight.Bold)
          }.borderRadius(20).backgroundColor('#FFF8DC').width('48%')
          .onClick(()=>{
            tcpConnect(this.localAddr,this.targetAddr)
            tcpSend(pullRequest.TEMPERATURE)
            this.recvMsg = tcpRecv()
            this.temperature = tcpRecv().substring(3,7)
          })

        }.height('15%').borderRadius(20).width('90%').margin({top:10})
      // 心率血氧、使用指导
        Row({space:20})
        {
          Column()
          {
            Row({space:20})
            {
              Image($r('app.media.ic_gallery_shortcut_favorite')).height('100%').width('20%').objectFit(ImageFit.Contain)
              Text(this.heartCount).fontSize(50).fontWeight(FontWeight.Bold)
              Text(this.heartSop+'%').fontSize(50).fontWeight(FontWeight.Bold)
            }.height('40%').justifyContent(FlexAlign.Start).width('100%')
            Text('心率 血氧').fontSize(40)

          }.borderRadius(20).backgroundColor('#FFFAFA').width('48%')
          .onClick(()=>{
            tcpConnect(this.localAddr,this.targetAddr)
            tcpSend(pullRequest.HEART)
            this.recvMsg = tcpRecv()
            this.heartCount = tcpRecv().substring(7,9)
            this.heartSop = tcpRecv().substring(9,11)
          })
          Column()
          {
            Image($r('app.media.coverpen')).height('50%').width('20%').objectFit(ImageFit.Contain)
            Text('使用指导').fontSize(40)
          }.borderRadius(20).backgroundColor('#F5F5F5').width('48%')
          .onClick(()=>{
            this.aboutPen = !this.aboutPen
          })

        }.height('15%').borderRadius(20).width('90%')

        // 设备连接Ip
        if(this.inputShow){
          Row()
          {
            TextInput({ placeholder: '请输入设备IP: '+tcpRecv()}).width('75%').height('10%').fontSize(30)
              .placeholderColor("rgb(0,0,225)")
              .placeholderFont({ size: 30, weight: 100, family: 'cursive', style: FontStyle.Italic })
              .onChange((value: string) => {
                this.InputIP = value
                this.targetAddr.address = value
              })
            Button('确认').height(60).margin({left:30}).width('10%')
              .onClick(()=>{
              tcpConnect(this.localAddr,this.targetAddr)
            })
          }
        }
      }.justifyContent(FlexAlign.Start)
      .width('100%')
  }
  // 先断开已有连接
  onPageShow(){
    tcpClose()
  }
}

多功能测量笔功能页面展示如下图:

2.3.3 多功能测量笔-硬件开发

为实现测量笔移动端数据传输,需要对硬件开发。多功能测量笔为自主设计,实现超声测身高、测温度、心率血氧测量。设计图与实物如下:

该部分涉及三款传感器的驱动开发,温度传感器使用Uart传输数据,mx30102心率血样使用ic2传输,超声波传感器使gpio直接驱动,更具声速测距的原理获取高度。目前已开发完传感器驱动,数据不稳定,仍在调试中。下面给出主要代码(max30102、socket开发参考本人早期分享帖: max30102开发 Hi3861与DAYU200socket):

//业务逻辑代码:
//数据帧: 身高(178) + 体温(36.5)+ 心率(62)+ 血氧(99)共11位字符
		unsigned char msg_cmd = '0';
		// 读取止血数据
		while (1)
		{
			if ((ret = recv(new_fd, recvbuf, sizeof(recvbuf), 0)) == -1)
			{
				printf("recv error \r\n");
			}
			printf("recv :%s\r\n", recvbuf);
			msg_cmd = recvbuf[0];
		    printf("msg_cmd: %c\n",msg_cmd);
			
			// 获取温度
		hi_uart_read(HI_UART_IDX_1,temp_buff,TEMP_SIZE);
			//hi_uart_read_timeout(HI_UART_IDX_1,temp_buff,BLOOD_SIZE,READ_TIME);  
			// temp_buff[0] = '0';
			// temp_buff[1] = '1';
			// temp_buff[2] = '2';
			// temp_buff[3] = '3';
	     	// printf("temp_buff: %s\n",temp_buff);
            strcat(buf,temp_buff);

			// // 获取高度
			// height = get_distance();
			// height = 456.7;
			// Float2String(height,height_buff,1);
			printf("height_buff: %s\n",height_buff);
			strcat(buf,height_buff);
			
			// 获取心率
            max_Data(&RED_channel_data, &IR_channel_data);
			// RED_channel_data =  88;
			Float2String(RED_channel_data,RED_buff,1);
			// IR_channel_data = 99;
			Float2String(IR_channel_data,IR_buff,1);
			// printf("RED_buff: %s\n",RED_buff);
			// printf("IR_buff: %s\n",IR_buff);
			strcat(buf,RED_buff);
			strcat(buf,IR_buff);

			// printf("buf= %s \n",buf);
           
			if ((ret = send(new_fd, buf, strlen(buf)+1, 0)) == -1)
			{
				perror("send : ");
			}
			buf[0] = '0';
		}

目前基本功能已实现,传感器数据稳定性有待提升,视频测试了将数据流传入DAYU200,如有下一阶段,将展示完整的测量效果,并添加oled显示功能。

【数据帧: 身高(178) + 体温(36.5)+ 心率(62)+ 血氧(99)共11位字符】

2.4 急救箱模块

2.4.1 急救知识教学模块

普及急救知识意义重大,科学的急救技能在危急时刻可化险为夷,软件中添加该模块意义即在于此。模块数据包括常用药物介绍、烫伤、海姆立克急救法等。这些数据均来自权威机构,但迫于版权,相关数据库目前仍在请求中。另外该模块还添加一台便携式肢体止血设备,在肢体大出血时可提供帮助,在2.4.2节详细介绍。下面先说明https请求数据的实现,为手续数据库同步做准备

// 权限、导入模块
import http from '@ohos.net.http';
  // 每一个httpRequest对应一个http请求任务,不可复用
  httpRequest:http.HttpRequest = http.createHttp();
  GetDate(){
    // 用于订阅http响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
    // 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
    prompt.showToast({message:'Request Begin'})
    this.httpRequest.request(
      // 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。
      // 请求的参数可以在extraData中指定
      "http://apis.juhe.cn/simpleWeather/query?key=397c9db4cb0621ad0313123dab416668&city=大连",
      {
        method: http.RequestMethod.GET, // 可选,默认为http.RequestMethod.GET
        // 开发者根据自身业务需要添加header字段
        header: {
          'Content-Type': 'application/json'
        },
        // 当使用POST请求时此字段用于传递内容
        extraData: {
          "data": "data to send",
        },
        connectTimeout: 100000, // 可选,默认为60s
        readTimeout: 100000, // 可选,默认为60s
      }, (err, data) => {
      if (!err) {
        // data.result为http响应内容,可根据业务需要进行解析
        console.info('Result:' + data.result);
        this.message = JSON.stringify(data.result)

        console.info('code:' + data.responseCode);
        // data.header为http响应头,可根据业务需要进行解析
        console.info('header:' + JSON.stringify(data.header));
        console.info('cookies:' + data.cookies); // 8+
        prompt.showToast({message:JSON.stringify(data.header)})
      } else {
        prompt.showToast({message:'Request Failed'})
        console.info('error:' + JSON.stringify(err));
        // 当该请求使用完毕时,调用destroy方法主动销毁。
        this.httpRequest.destroy();
      }
    }
    );
  }

// 展示请求到的数据(一般是key、value):
           Text("http 请求")
             .fontSize(50)
             .fontWeight(FontWeight.Bold)
             .onClick(()=>
             {
               this.GetDate();
              })

2.4.2 便携式肢体止血-软件开发

便携式肢体止血设备软件页面如下图所示,整体布局与多功能测量笔类似,这里不再重复说明页面框架。该页面展示当前止血压力、持续时间,用户可通过软件查看,也可设置时间,当然在紧急情况下设备单独使用最为可靠,设备自带调压按钮,可独立工作。

2.4.3 便携式肢体止血-硬件开发

止为什么要开发止血设备呢? 因为止血过程中存在止血压力过大(小)、时间过长,引起肢体坏死或失血过多。参考相关文献【[1]气压止血带在四肢手术中应用的专家共识协作组. 气压止血带在四肢手术中应用的专家共识[J]. 中华麻醉学杂志, 2020, 40(10):7.】可知,止血压力、时间需要进行一定控制。

  • 设计的止血设备即是解决上述需求,可独立工作、止血压力可控,最大止血压力达50kPa,针对上肢完全冗余。设计结构与原理图如下:

为提升可靠性,hi3861仅作为中间模块,作为交互数据硬件介质,底层硬件控制完全依靠另一款mcu实现,可根据外部请求返回数据或执行相应动作。流程如下:

socket、uart在前文中已提到如何开发,这里不再赘述。hi3861业务逻辑代码如下:

		while (1)
		{
			if ((ret = recv(new_fd, recvbuf, sizeof(recvbuf), 0)) == -1)
			{
				printf("recv error \r\n");
			}
			printf("recv :%s\r\n", recvbuf);
			msg_cmd = recvbuf[0];
			
		    printf("msg_cmd: %c\n",msg_cmd);
			switch (msg_cmd)
			{
			case 'p':{  // 压力
				msg_cmd = BEGIN;
				hi_uart_write(HI_UART_IDX_1, &msg_cmd, CMD_LEN);
			break;
			}
	 		case 't':{ // 时间
				 msg_cmd = CONNECT ; 
				hi_uart_write(HI_UART_IDX_1, &msg_cmd, CMD_LEN);
			break;
			}			
			default:
				break;
			}
			hi_uart_read(HI_UART_IDX_1,uart_buff,BLOOD_SIZE);
			//hi_uart_read_timeout(HI_UART_IDX_1,uart_buff,BLOOD_SIZE,READ_TIME);  
	     	printf("uart_buff: %s\n",uart_buff);   

			if ((ret = send(new_fd, uart_buff, BLOOD_SIZE, 0)) == -1)
			{
				perror("send : ");
			}
			uart_buff[0] = '0';
		}

2.5 康复训练手套开发

该设备软件层面与其他设备类似,这里不再赘述。训练手套的思路是UI端下发训练强度指令,分为三种不同训练强度,硬件端hi3861接收到指令执行训练动作。下图是实物,演示见视频demo。

2.6 其他功能

除上述核心功能外,家庭活动(公益献血、健康教育等)、疫情防控消息、天气获取等小功能也有准备,奈何目前数据仅拿到天气API,疫情消息目前加载网易公开信息,拿到可用API可做一个精简页面。

// 和风实况天气 接口免费
@Component
struct FullWeather{
  controller: WebController = new WebController();
  build() {
    Column()
    {
            Web({ src: 'https://widget-page.qweather.net/h5/index.html?md=0123456&bg=1&lc=auto&key=a400290f961647a884e98675bf8954d8&v=_1660208701535',
              controller: this.controller })
    }
    .width('25%')
    .height('100%')
  }
}

3. 家庭健康助理demo

作品视频:  家庭健康助理

 想了解更多关于开源的内容,请访问:

 51CTO 开源基础软件社区

 https://ost.51cto.com/#bkwz


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK