# 家庭医生终端系统 **Repository Path**: mamab4life/family-doctor-terminal-system ## Basic Information - **Project Name**: 家庭医生终端系统 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2025-01-21 - **Last Updated**: 2025-01-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 家庭医生终端系统-中控系统 家庭医生终端系统是一站式家庭常规医疗需求解决方案,提出在线问诊、家庭急救、慢性病追踪等功能,以方便快捷为核心,结合最新的1+N多系统设备联合技术共同保障家庭成员的身体健康。 演示视频:https://www.bilibili.com/video/BV1Su4y1g7aW # 一、项目介绍 1 ## (1)基于DAYU800-RISCV开发一种家庭医生系统,功能如下: 1、一个终端+多个元服务(openharmony不好演示所以暂时使用APP演示) 2、慢性病管理:具有吃药提醒功能(高血压/降压药),提供在线问诊的方式实现用药指导功能。 3、紧急呼叫:针对家庭中可能会出现的急救场景,提供单个页面进行急救知识的普及;具有一键报警功能,实现快速发送当前现场照片/现场录音,并得到远方技术指导照片和语音指示; 4、居住环境监测:根据子设备传来的数据实现水质TDS监测和空气质量监测。 5、人体健康监测:血压、血氧、心率、体重、体脂的显示和对应医学建议。 6、分布式心率检测:利用openharmony分布式能力,实现心率多设备同步。 7、养生贴士:每日更新养生小贴士,小知识点实现大健康生活。 8、女性生理周期记录:可提供易孕时期、排卵时期、月经时期的预测; 9、在线问诊:设计在线问诊功能,若有头疼脑热等不良症状,可实现与后台对接,与医师直接沟通,使得病情得到更快的控制,同时因为方便程度减少了“百度看病”,减少了自我误判导致病情加剧的可能性。 10、医疗商城:构想实现药物、医疗设备和养身课程的购买,实现多种盈利方式。 ## (2)监测设备展示 * 空气环境监测:PM2.5+温湿度 * 体重体脂测量:应变片测量体重 * 血压心率测量:心率、血压、血氧饱和度 * 水质检测:饮用水TDS数据测量 # 二、作品得分点 ## 1.参赛作品是否具备创新性 20分 > 家庭医生终端系统部署示意图 > 家庭急救功能示意图 > 慢性病跟踪示意图 > 在线问诊与医疗商城 #### 提出家庭医生终端概念,其是一站式家庭常规医疗需求解决方案,用户可依靠该终端搭建一套完整的身体监测系统。 * 1+N架构:使用1个APP+N个元服务的形式,单点功能选择元服务和卡片的形式快速上线,可以独立运行又可以使用APP进行跳转。 * 使用1+N的项目框架和1+N的程序框架,实现一个终端+多个设计、一个鸿蒙应用+多个元服务共同作用。 * 数字看病:使用数据反馈到后台,以数据为依据进行诊断,避免纯主观的"你问我答",增加问诊的效率。 * 慢性病检测:可根据设备检测的实际数据实现慢性病长期监测,通过后台分析,实现动态控药。 * 家庭急救:发生紧急事件时快速响应(大大降低了因外伤、心脏病等情况造成的意外死亡),避免病情或者意外恶化。 * AI医生:接入人工智能,方便小病更及时的获取医疗建议。 ## 2.实用性及产品落地应用可行性 20分 ### (1)实用性 10分 * 多种健康参数数据显示:水质、体重体脂、血压、空气污染指数等; * 吃药提醒:可设置三个闹钟,振动+通知提醒,防止忘记吃药。 * 女生经期管理:可记录当前经期情况,下一步会提供易孕时期、排卵时期、月经时期的预测。 * AI医生:接入人工智能,方便小病小痛更及时的获取医疗建议。 * 在线医生:医生后台挂诊,实现文字、图片、语音多维度病情探测。 * 家庭急救:提供烧伤、触电、外伤、中毒四种急救的快速处理方式,并通过视频普及急救知识。 * 医疗商城:可提供医疗设备的订购与使用。 ### (2)落地可行性 10分 * 当前人民对生活的温饱重心已经提高到了生活水平上,而身体健康是一切的根本,医疗产品的前景颇好。 * 提供多维度的数据检测功能,常见的血压、体重、水质等等。 * 在线问诊:足不出户,即可治病。 ## 3.参赛作品是否体现了的 OpenHarmony 相关特性 30分 ### (1)分布式能力 5分 * 在应用内集成了设备互信认证,实现设备的配对 * 使用分布式数据对象,将数据进行分布式处理,保障安全性 ### (2)元服务 5分 开发了两个元服务:在线医生与运动健康,且都具有卡片能力 ### (3)一次开发多端部署 **分 * 多个页面支持一次开发多端部署,在横屏和竖屏下呈现不同的布局效果 ### (4)动态卡片 **分 * 目前共有三个桌面卡片,其中两个是跳转还有一个是桌面动态刷新 ### (5)AGC云端能力(手机认证)*分 * 使用AGC的设备认证能力,提供手机号登录 ## 4.参赛团队是否提供足够详细的说明文档指导其他开发者运行/使用这个项目 30分 本项目会提供完整的页面设计和、服务器设计和功能设计文档(详细可参看如下); ### (1)界面设计:包括一些组件的使用和注意事项 ### (2)功能设计:包括核心功能逻辑和示例程序 ### (3)设备端设计[Hi3861]:包括HI3861的驱动逻辑和代码 # 三、界面设计 ## (1)主页面选择(Tabs) ![](Image/2.gif) 该组件可将界面分为多个页面(如上图所示),每个角标可设置对应的图标和文字。 官方文档链接:https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-tabcontent.md/ ```c // xxx.ets @Entry @Component struct TabContentExample { @State fontColor: string = 'rgba(0, 0, 0, 0.4)' @State selectedFontColor: string = 'rgba(10, 30, 255, 1)' @State currentIndex: number = 0 private controller: TabsController = new TabsController() @Builder TabBuilder(index: number) { Column() { Image(this.currentIndex === index ? '点击前图片' : '点击后图片') .width(10) .height(10) .opacity(this.currentIndex === index ? 1 : 0.4) .objectFit(ImageFit.Contain) Text(`Tab${(index > 2 ? (index - 1) : index) + 1}`) .fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor) .fontSize(10) .margin({top: 2}) } } build() { Column() { Tabs({ barPosition: BarPosition.End, controller: this.controller }) { TabContent() { Flex({justifyContent: FlexAlign.Center}) { Text('Tab1').fontSize(32) } }.tabBar(this.TabBuilder(0)) TabContent() { Flex({justifyContent: FlexAlign.Center}) { Text('Tab2').fontSize(32) } }.tabBar(this.TabBuilder(1)) } .vertical(false) .barWidth(300).barHeight(56) .onChange((index: number) => { this.currentIndex = index }) .width('90%').backgroundColor('rgba(241, 243, 245, 0.95)') }.width('100%').height(200).margin({ top: 5 }) } } ``` 如上方,可实现同页面下两个选项显示。 ## (2)水质监控图表(Gauge) ![](Image/8.png) 该组件可实现将数据图形化,把number类型数据转化为多段图形,更能直观的表现当前数值的大小和数值大小对应的程度。 官方文档链接:https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-gauge.md/ ```javascript Gauge({ value: 50, min: 0, max: 100 }) .startAngle(210).endAngle(150) .colors([[0x317AF7, 1], [0x5BA854, 1], [0xE08C3A, 1], [0x9C554B, 1], [0xD94838, 1]]) .strokeWidth(20) .width(200).height(200) ``` ## (3)闹钟设置(Toggle,TimePicker) 其中Toggle是勾选框样式、状态按钮样式及开关样式。TimePicker可以对时间进行滚动。 官方文档链接: Toggle:https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-toggle.md/ TimePicker:https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-timepicker.md/ ![](Image/3.gif) ![](Image/4.gif) 这里选择一个Row组件,实现横着摆放文字+时间选择器+Toggle ```ts Row() { Text('设置闹钟时间(24小时):') .fontSize(CONSTANTS.FONT_SIZE_16) TimePicker() .width(CONSTANTS.WIDTH_20) .height("15%") .useMilitaryTime(true) //.offset({ x: 0, y: -8 }) .onChange((value) => { this.time_picker_result[reminderId][0] = value.hour this.time_picker_result[reminderId][1] = value.minute }) Text('分') .fontSize(CONSTANTS.FONT_SIZE_16) Blank() Toggle({ type: ToggleType.Switch }) .onChange((is_on) => { }) } .width(CONSTANTS.WIDTH_100) ``` ## (4)设备呈现(支持一多) ### GridRow 栅格布局可以为布局提供规律性的结构,解决多尺寸多设备的动态布局问题,保证不同设备上各个模块的布局一致性。 ![](Image/9.png) ```ts Column() { GridRow({ columns: 5, gutter: { x: 5, y: 10 }, breakpoints: { value: ["400vp", "600vp", "800vp"], reference: BreakpointsReference.WindowSize }, direction: GridRowDirection.Row }) { ForEach(this.bgColors, (color: Color) => { GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 }, offset: 0, order: 0 }) { Row().width("100%").height("20vp") }.borderColor(color).borderWidth(2) }) }.width("100%").height("100%") .onBreakpointChange((breakpoint) => { this.currentBp = breakpoint }) }.width('80%').margin({ left: 10, top: 5, bottom: 5 }).height(200) .border({ color: '#880606', width: 2 }) ``` 在本项目中,需要ForEach的是设备,这个设备已经被封装成了一个对象,是UI的集合 ![](Image/10.png) ```ts @Builder Device(device: Devices, img: Resource, index: number) { if (1 == device.type) { Column() { Gauge({ value: this.data, min: 0, max: 1000 }) .startAngle(210) .endAngle(150) .colors([[0x317AF7, 1], [0x5BA854, 1], [0xE08C3A, 1], [0x9C554B, 1], [0xD94838, 1]]) .strokeWidth(25) .height(CONSTANTS.HEIGHT_60) Text(`${device.introduce} ${this.data}`) .height(CONSTANTS.HEIGHT_15) this.StateBuilder(`设备0${index + 1}在线状态`) } .width(CONSTANTS.WIDTH_100) .padding(CONSTANTS.PADDING_20) .height(CONSTANTS.HEIGHT_50) .onAppear(() => { this.intervalId = setInterval(() => { animateTo({ curve: Curve.Friction, duration: CONSTANTS.DURATION_1000 }, () => { if(this.data<900){ this.data=this.data+20; }else { this.data = 100; } //this.data = Math.floor(Math.random() * 1000) }) }, CONSTANTS.DURATION_500) }) .onDisAppear(() => { clearInterval(this.intervalId) }) } else { Column() { Row({ space: CONSTANTS.SPACE_20 }) { Image(img) .width(CONSTANTS.SIZE_150) .height(CONSTANTS.SIZE_150) .objectFit(ImageFit.Contain) Column({ space: CONSTANTS.SPACE_10 }) { ForEach(device.data, (item: string) => { Text(item) .fontWeight(FontWeight.Medium) }) } .width(CONSTANTS.WIDTH_47) .height(CONSTANTS.HEIGHT_60) .alignItems(HorizontalAlign.Start) .justifyContent(FlexAlign.Center) } .justifyContent(FlexAlign.Center) Text(device.introduce) .height(CONSTANTS.HEIGHT_15) this.StateBuilder(`设备0${index + 1}在线状态`) } .width(CONSTANTS.WIDTH_100) .height(CONSTANTS.HEIGHT_50) .padding(CONSTANTS.PADDING_20) .onClick(async () => { }) } } ``` ## (5)下拉刷新 ![](Image/5.gif) Refresh 可以进行页面下拉操作并显示刷新动效的容器组件。 > **说明:** > > 该组件从API Version 8开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。 > > 当设置自定义组件时,自定义组件的高度限制在64vp之内。 ```ts Refresh({ refreshing: $$this.isRefreshing, offset: 120, friction: 100 }) { Text('Pull Down and refresh: ' + this.counter) .fontSize(30) .margin(10) } .onStateChange((refreshStatus: RefreshStatus) => { console.info('Refresh onStatueChange state is ' + refreshStatus) }) .onRefreshing(() => { setTimeout(() => { this.counter++ this.isRefreshing = false }, 1000) console.log('onRefreshing test') }) ``` ## (6)动画插入 ![](Image/6.gif) 提供全局animateTo显式动画接口来指定由于闭包代码导致的状态变化插入过渡动效。同属性动画,布局类改变宽高的动画,内容都是直接到终点状态,例如文字、canvas的内容、linearGradient等,如果要内容跟随宽高变化,可以使用[renderFit](https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/reference/arkui-ts/ts-universal-attributes-renderfit.md/)属性配置。 ```ts animateTo({ duration: 1200, curve: Curve.Friction, delay: 500, iterations: -1, // 设置-1表示动画无限循环 playMode: PlayMode.Alternate, onFinish: () => { console.info('play end') } }, () => { this.rotateAngle = 90 }) ``` # 四、功能设计 ## (1)权限申请 在基础UI和TCP上无需调用权限,单当使用复杂功能(文件读写、摄像头、地理位置、录音等)需要在系统中声明权限,而官方的权限声明有时没注意到容易产生该问题(造成程序没问题但无法实现功能),新人容易在此卡坑 先看下官方的权限定义:https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/security/permission-list.md/ 如果需要修改,请在module.json5中修改,其位置是"module"下新建"reqPermissions",如下: ```js "reqPermissions": [ { "name": "ohos.permission.MICROPHONE" }, { "name": "ohos.permission.CAMERA" }, { "name": "ohos.permission.MEDIA_LOCATION" }, { "name": "ohos.permission.WRITE_MEDIA" }, { "name": "ohos.permission.READ_MEDIA" }, { "name": "ohos.permission.INTERNET" } ] ``` 以上是申请了麦克风、摄像头、本地图库、媒体读写和网络访问(个别访问API使用)的权限。 除了在json5文件中申请,一般会在应用打开时进行一次动态申请,常用的动态申请方式如下: ```ts import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'; const permissions: Array = [ 'ohos.permission.READ_MEDIA', 'ohos.permission.WRITE_MEDIA', 'ohos.permission.MEDIA_LOCATION', 'ohos.permission.INTERNET', 'ohos.permission.MICROPHONE' ]; //在主程序中调用aboutToAppear函数进行动态声明 aboutToAppear() { this.reqPermissionsFromUser(permissions) } ``` ## (2)视频播放(AVplar) - [AVPlayer](https://docs.openharmony.cn/pages/v3.2/zh-cn/application-dev/media/using-avplayer-for-playback.md/):功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。 AVPlayer需要搭配[XComponent](https://docs.openharmony.cn/pages/v3.2/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-xcomponent.md/)进行使用,这里提供一个最简单播放本地rawfile的例程,放置XComponent后和一个按钮,按钮按下进行播放 ```ts import media from '@ohos.multimedia.media'; import fs from '@ohos.file.fs'; import common from '@ohos.app.ability.common'; // @ts-ignore import { BusinessError } from '@ohos.base'; export class AVPlayerDemo { private count: number = 0; private surfaceID: string = ''; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法 private isSeek: boolean = true; // 用于区分模式是否支持seek操作 private fileSize: number = -1; private fd: number = 0; // 注册avplayer回调函数 setAVPlayerCallback(avPlayer: media.AVPlayer) { // seek操作结果回调函数 avPlayer.on('seekDone', (seekDoneTime: number) => { console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); }) // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程 avPlayer.on('error', (err: BusinessError) => { console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); avPlayer.reset(); // 调用reset重置资源,触发idle状态 }) // 状态机变化回调函数 avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => { switch (state) { case 'idle': // 成功调用reset接口后触发该状态机上报 console.info('AVPlayer state idle called.'); avPlayer.release(); // 调用release接口销毁实例对象 break; case 'initialized': // avplayer 设置播放源后触发该状态上报 console.info('AVPlayer state initialized called.'); avPlayer.surfaceId = globalThis.setfaceid; // 设置显示画面,当播放的资源为纯音频时无需设置 avPlayer.prepare(); break; case 'prepared': // prepare调用成功后上报该状态机 console.info('AVPlayer state prepared called.'); avPlayer.play(); // 调用播放接口开始播放 break; case 'playing': // play成功调用后触发该状态机上报 this.count++; break; case 'paused': // pause成功调用后触发该状态机上报 console.info('AVPlayer state paused called.'); //avPlayer.play(); // 再次播放接口开始播放 break; case 'completed': // 播放结束后触发该状态机上报 console.info('AVPlayer state completed called.'); avPlayer.stop(); //调用播放结束接口 break; case 'stopped': // stop接口成功调用后触发该状态机上报 console.info('AVPlayer state stopped called.'); avPlayer.reset(); // 调用reset接口初始化avplayer状态 break; case 'released': console.info('AVPlayer state released called.'); break; default: console.info('AVPlayer state unknown called.'); break; } }) } // 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例 async avPlayerFdSrcDemo() { // 创建avPlayer实例对象 let avPlayer: media.AVPlayer = await media.createAVPlayer(); // 创建状态机变化回调函数 this.setAVPlayerCallback(avPlayer); // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址 // 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度 let context = getContext(this) as common.UIAbilityContext; let fileDescriptor = await context.resourceManager.getRawFd('test1.mp4'); let avFileDescriptor: media.AVFileDescriptor = { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length }; // 为fdSrc赋值触发initialized状态机上报 avPlayer.fdSrc = avFileDescriptor; } } @Entry @Component struct Index { @State message: string = 'Hello World' private surfaceId : string ='' private LALlayerDemo: AVPlayerDemo = new AVPlayerDemo(); xcomponentController: XComponentController = new XComponentController() controller: VideoController = new VideoController() build() { Row() { Column() { XComponent({ id: 'xcomponent', type: 'surface', controller: this.xcomponentController }) .width("100%") .height("50%") .onLoad(() => { this.xcomponentController.setXComponentSurfaceSize({surfaceWidth:600,surfaceHeight:600}); this.surfaceId = this.xcomponentController.getXComponentSurfaceId() globalThis.setfaceid = this.surfaceId }) Row() { Button('点击播放') .type(ButtonType.Normal) .onClick(async () => { await this.LALlayerDemo.avPlayerFdSrcDemo() }) }.width("100%") .height("20%") } .width('100%') } .height('100%') } ``` ## (3)声音录制 - [AVRecorder](https://docs.openharmony.cn/pages/v3.2/zh-cn/application-dev/media/using-avrecorder-for-recording.md/):功能较完善的音频、视频录制ArkTS/JS API,集成了音频输入录制、音频编码和媒体封装的功能。开发者可以直接调用设备硬件如麦克风录音,并生成m4a音频文件。 本项目主要使用到的场景为语音消息的录制。录制格式为m4a,提供封装好的开始录制与停止录制接口。 ```ts export class AudioService { private static avRecorder; private static avProfile = { audioBitrate: 100000, // 音频比特率 audioChannels: 2, // 音频声道数 audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac audioSampleRate: 48000, // 音频采样率 fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a }; private static avConfig = { audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_DEFAULT, // 音频输入源,这里设置为麦克风 profile: AudioService.avProfile, url: 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件 }; // 注册audioRecorder回调函数 static setAudioRecorderCallback() { // 状态机变化回调函数 this.avRecorder.on('stateChange', (state, reason) => { console.log(`AudioRecorder current state is ${state}`); // AlertDialog.show({ // message: JSON.stringify(state+reason), // }) }) // 错误上报回调函数 this.avRecorder.on('error', (err) => { console.error(`AudioRecorder failed, code is ${err.code}, message is ${err.message}`); }) } // 开始录制对应的流程 static async startRecordingProcess() { // 1.创建录制实例 this.avRecorder = await media.createAVRecorder(); this.setAudioRecorderCallback(); // 2.获取录制文件fd赋予avConfig里的url;参考FilePicker文档 // 3.配置录制参数完成准备工作 // 固定一个文件存放地点 let file = fs.openSync(globalThis.lalcontext.cacheDir + '/test.m4a', fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); this.avConfig.url = 'fd://' + file.fd.toString(); await this.avRecorder.prepare(this.avConfig); // 4.开始录制 await this.avRecorder.start(); } // 暂停录制对应的流程 static async pauseRecordingProcess() { if (this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换 await this.avRecorder.pause(); } } // 恢复录制对应的流程 static async resumeRecordingProcess() { if (this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换 await this.avRecorder.resume(); } } // 停止录制对应的流程 static async stopRecordingProcess() { // 1. 停止录制 if (this.avRecorder.state === 'started' || this.avRecorder.state === 'paused') { // 仅在started或者paused状态下调用stop为合理状态切换 await this.avRecorder.stop(); } // 2.重置 await this.avRecorder.reset(); // 3.释放录制实例 await this.avRecorder.release(); // 4.关闭录制文件fd } } ``` ## (4)声音播放 - [AVPlayer](https://docs.openharmony.cn/pages/v3.2/zh-cn/application-dev/media/using-avplayer-for-playback.md/):功能较完善的音频、视频播放ArkTS/JS API,集成了流媒体和本地资源解析、媒体资源解封装、音频解码和音频输出功能。可以用于直接播放mp3、m4a等格式的音频文件,不支持直接播放PCM格式文件。 - 本项目主要使用到的地方为录音的播放,格式是m4a,提供封装好的播放接口。 ```ts export class AVPlayerService { private static avPlayer; private static count: number = 0; // 注册avplayer回调函数 static setAVPlayerCallback() { // seek操作结果回调函数 this.avPlayer.on('seekDone', (seekDoneTime) => { console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); }) // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程 this.avPlayer.on('error', (err) => { console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); this.avPlayer.reset(); // 调用reset重置资源,触发idle状态 }) // 状态机变化回调函数 this.avPlayer.on('stateChange', async (state, reason) => { switch (state) { case 'idle': // 成功调用reset接口后触发该状态机上报 console.info('AVPlayer state idle called.'); this.avPlayer.release(); // 调用release接口销毁实例对象 break; case 'initialized': // avplayer 设置播放源后触发该状态上报 console.info('AVPlayerstate initialized called.'); // AlertDialog.show({ // message: JSON.stringify('set uar http'), // }) this.avPlayer.prepare().then(() => { console.info('AVPlayer prepare succeeded.'); }, (err) => { console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`); }); break; case 'prepared': // prepare调用成功后上报该状态机 console.info('AVPlayer state prepared called.'); this.avPlayer.play(); // 调用播放接口开始播放 break; case 'playing': // play成功调用后触发该状态机上报 console.info('AVPlayer state playing called.'); break; case 'paused': // pause成功调用后触发该状态机上报 console.info('AVPlayer state paused called.'); //this.avPlayer.play(); // 再次播放接口开始播放 break; case 'completed': // 播放结束后触发该状态机上报 console.info('AVPlayer state completed called.'); this.avPlayer.stop(); //调用播放结束接口 break; case 'stopped': // stop接口成功调用后触发该状态机上报 console.info('AVPlayer state stopped called.'); this.avPlayer.reset(); // 调用reset接口初始化avplayer状态 break; case 'released': console.info('AVPlayer state released called.'); break; default: console.info('AVPlayer state unknown called.'); break; } }) } // 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过url属性进行播放示例 static async avPlayerUrlDemo(fir_diar:String) { // 创建avPlayer实例对象 this.avPlayer = await media.createAVPlayer(); // 创建状态机变化回调函数 this.setAVPlayerCallback(); this.avPlayer.url = fir_diar } } ``` ## (5)文件HTTP上传 在OpenHarmony系统中,目前文件的上传依然有点小瑕疵,不可以直接进行上传,需要先转存到缓存区再进行二次上传,此处提供一个实现方式: ```ts static async send_file() { let file_lal = null; //将文件转存到缓存区 fs.open(globalThis.url_file, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE).then((file) => { console.info("file fd: " + file.fd); file_lal = file; let delete_dir = globalThis.lalcontext.cacheDir + "/testt.jpg"; fs.unlink(delete_dir).then(() => { console.info("remove file succeed"); }).catch((err) => { console.info("remove file failed with error message: " + err.message + ", error code: " + err.code); }); //将文件转存到缓存区 fs.copyFile(file_lal.fd, globalThis.lalcontext.cacheDir + '/testt.jpg', async (err) => { if (err) { AlertDialog.show({ message: JSON.stringify(err) + '\r\n', }) console.info("copy file failed with error message: " + err.message + ", error code: " + err.code); } else { let uploadConfig = { url: 'http://' + this.the_ip + ':2233/', header: { key1: 'Content-Type', key2: 'multipart/form-data', Filename: this.file_name_str + ".jpg" }, //header: { key1: 'Content-Type', key2: 'multipart/form-data',Filename:"22.jpg" }, method: 'POST', files: [ { filename: 'test.jpg', name: 'test', uri: "internal://cache/" + 'testt.jpg', type: 'jpg' } ], data: [ { name: 'Filename', value: 'FP000008' } ] } //进行HTTP上传 try { request.uploadFile(globalThis.lalcontext, uploadConfig) .then((uploadTask) => { uploadTask.on('complete', (taskStates) => { for (let i = 0; i < taskStates.length; i++) { console.info(`xx upload complete taskState: ${JSON.stringify(taskStates[i])}`); } }); AlertDialog.show({ message: JSON.stringify("上传成功") + '\r\n', }) }) .catch((err) => { console.error(`xx Invoke uploadFile failed, code is ${err.code}, message is ${err.message}`); }) } catch (err) { console.error(`xx Invoke uploadFile failed, code is ${err.code}, message is ${err.message}`); } console.info("copy file success"); } }); }).catch((err) => { console.info("open file failed with error message: " + err.message + ", error code: " + err.code); AlertDialog.show({ message: JSON.stringify(err), }) }); } ``` ## (6)图片选择器picker 选择器(Picker)是一个封装PhotoViewPicker、DocumentViewPicker、AudioViewPicker等系统应用选择与保存能力的模块。应用可以自行选择使用哪种picker实现文件选择和文件保存的功能。 本项目在选择图片发送的时候需要用到图片图片选择器picker,其精简代码如下 ```ts import picker from '@ohos.file.picker'; function Choose_img() { let PhotoSelectOptions = new picker.PhotoSelectOptions(); PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; PhotoSelectOptions.maxSelectNumber = 1; let photoPicker = new picker.PhotoViewPicker(); await photoPicker.select(PhotoSelectOptions).then(async (PhotoSelectResult) => { globalThis.url_file = PhotoSelectResult.photoUris[0]; }).catch((err) => { return console.error('PhotoViewPicker.select failed with err: ' + err); }); } ``` ## (7)full_sdk的使用【系统应用】 **public-SDK**是提供给应用开发的工具包,跟随DevEco Studio下载,不包含系统应用所需要的高权限API **full-SDK**是提供给OEM厂商开发应用的工具包,不能随DevEco Studio下载,包含了系统应用所需要的高权限API 三方开发者通过DevEco Studio自动下载的**API8版本**SDK均为**public版本**。public-SDK**不支持**开发者使用**所有的系统API**,包括animator组件、xcomponent组件、@ohos.application.abilityManager.d.ts、@ohos.application.formInfo.d.ts、@ohos.bluetooth.d.ts等,如工程必须依赖于系统API,请按照以下步骤替换**full-SDK**。 1、下载full-SDK full-SDK需要手动下载。请参考[版本说明书](https://docs.openharmony.cn/pages/v3.2/zh-cn/release-notes/OpenHarmony-v3.2-beta2.md/)中的获取方式,从镜像站点获取所需的操作系统的full-SDK。 2、确认SDK路径 查看本地SDK路径(此处以ets工程为例,1.0工程请以相同方式替换js-SDK) 3、替换SDK a.解压已下载的full-SDK文件:`ets-windows-3.x.x.x-Release.zip` b.替换SDK文件 **备份本地SDK**(复制并重命名ets目录下版本信息目录名,或者将ets目录拷贝至其他本地路径) 打开获取到的本地已安装SDK文件路径并进行备份。 注意:需要从每日构建进行下载,本项目使用的full_sdk下载链接为:http://download.ci.openharmony.cn/version/Release_Version/OpenHarmony-3.2.15.3/20230929_132111/version-Release_Version-OpenHarmony-3.2.15.3-20230929_132111-ohos-sdk-full.tar.gz ## (8)分布式数据对象跨设备数据同步 - **分布式内存数据库** 分布式内存数据库将数据缓存在内存中,以便应用获得更快的数据存取速度,不会将数据进行持久化。若数据库关闭,则数据不会保留。 - **分布式数据对象** 分布式数据对象是一个JS对象型的封装。每一个分布式数据对象实例会创建一个内存数据库中的数据表,每个应用程序创建的内存数据库相互隔离,对分布式数据对象的“读取”或“赋值”会自动映射到对应数据库的get/put操作。 在使用本功能时需要用到full_sdk,如何使用请看上文,分布式对象的部分使用比较简单,给出基本例程: ```js // 导入模块 import distributedDataObject from '@ohos.data.distributedDataObject'; import UIAbility from '@ohos.app.ability.UIAbility'; class EntryAbility extends UIAbility { onWindowStageCreate(windowStage) { // 创建对象,该对象包含4个属性类型:string、number、boolean和Object let localObject = distributedDataObject.create(this.context, { name: 'jack', age: 18, isVis: false, parent: { mother: 'jack mom', father: 'jack Dad' }, list: [{ mother: 'jack mom' }, { father: 'jack Dad' }] }); } } // 设备1加入sessionId let sessionId = '123456'; localObject.setSessionId(sessionId); // 和设备1协同的设备2加入同一个session // 创建对象,该对象包含4个属性类型:string、number、boolean和Object let remoteObject = distributedDataObject.create(this.context, { name: undefined, age: undefined, // undefined表示数据来自对端 isVis: true, parent: undefined, list: undefined }); // 收到status上线后remoteObject同步数据,即name变成jack,age是18 remoteObject.setSessionId(sessionId); function changeCallback(sessionId, changeData) { console.info(`change: ${sessionId}`); if (changeData !== null && changeData !== undefined) { changeData.forEach(element => { console.info(`The element ${localObject[element]} changed.`); }); } } // 发起方要在changeCallback里刷新界面,则需要将正确的this绑定给changeCallback localObject.on("change", this.changeCallback.bind(this)); localObject.name = 'jack1'; localObject.age = 19; localObject.isVis = false; localObject.parent = { mother: 'jack1 mom', father: 'jack1 Dad' }; localObject.list = [{ mother: 'jack1 mom' }, { father: 'jack1 Dad' }]; ``` 需要注意的是localObject.name,这种赋值大概率会报错,需要使用到// @ts-ignore,如下 ```js // @ts-ignore this.mDistributedObject.rates = rates // @ts-ignore this.mDistributedObject.rate = rate // @ts-ignore this.mDistributedObject.second = sec ``` ## (9)系统通知 基础类型通知主要应用于发送短信息、提示信息、广告推送等,支持普通文本类型、长文本类型、多行文本类型和图片类型。 **表1** 基础类型通知中的内容分类 | 类型 | 描述 | | :------------------------------ | :------------- | | NOTIFICATION_CONTENT_BASIC_TEXT | 普通文本类型。 | | NOTIFICATION_CONTENT_LONG_TEXT | 长文本类型。 | | NOTIFICATION_CONTENT_MULTILINE | 多行文本类型。 | | NOTIFICATION_CONTENT_PICTURE | 图片类型。 | ```js import notificationManager from '@ohos.notificationManager'; let notificationRequest: notificationManager.NotificationRequest = { id: 1, content: { contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 普通文本类型通知 normal: { title: 'test_title', text: 'test_text', additionalText: 'test_additionalText', } } }; notificationManager.publish(notificationRequest, (err) => { if (err) { console.error(`Failed to publish notification. Code is ${err.code}, message is ${err.message}`); return; } console.info('Succeeded in publishing notification.'); }); ``` ## (10)基础数据传输 本项目的数据传输大多使用TCP来实现,这里通过构造类实现,其基础使用为下面: ```js export class NetService { static get_time = Math.floor(Math.random() * 10000) static flag: boolean = false static tcp = socket.constructTCPSocketInstance(); static the_ip = '192.168.3.14' //static the_ip = '117.72.12.94' // 每一个httpRequest对应一个HTTP请求任务,不可复用 static http_request_chat = http.createHttp(); static file_name_str = '' static mic_url_string = '' // 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息 // 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+ static Bind() { this.tcp.bind({ address: '0.0.0.0', port: 12121, family: 1 }, err => { if (err) { console.log('bind fail'); return; } console.log('bind success'); }) this.tcp.on('close', () => { console.log("on close") this.flag = false; }); } static 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; } } static Message() { this.tcp.on('message', value => { //console.log(value.message); console.log("on message, message:" + value.message + ", remoteInfo:" + value.remoteInfo) let da = this.resolveArrayBuffer(value.message); let dat_buff = String(da); let buffer = value.message let dataView = new DataView(buffer) let str = "" for (let i = 0; i < dataView.byteLength; ++i) { str += String.fromCharCode(dataView.getUint8(i)) } let get_json = JSON.parse(str); console.info(`get_json:${JSON.stringify(get_json)}`) if (get_json.mode == 're_txt') { globalThis.chat_txt = get_json.chat let send_id = get_json.send_id } else if (get_json.mode == 're_img') { //AppStorage.Set('REC_text', get_json.chat); globalThis.image_url = 'http://' + this.the_ip + ':2233/' + get_json.img + '.jpg' let send_id = get_json.send_id } else if (get_json.mode == 're_mic') { //AppStorage.Set('REC_text', get_json.chat); globalThis.mic_url = 'http://' + this.the_ip + ':2233/' + get_json.mic + '.m4a' let send_id = get_json.send_id } }); } static send_once(Con_buff) { if (this.flag == false) { let promise = this.tcp.connect({ address: { address: this.the_ip, port: 3344, family: 1 }, timeout: 100 }); promise.then(() => { console.log('connect success'); this.flag = true; this.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 (this.flag == true) { this.tcp.send({ data: Con_buff }, err => { if (err) { console.log('send fail'); return; } console.log('send success'); }) } } static login_user(name: String) { // @ts-ignore let login_buf: chat_list = { mode: "login", user: name }; this.send_once(JSON.stringify(login_buf)); } static Send_TxT(user: String, data: String) { // @ts-ignore //let TXT_buf: chat_list = { mode: "txt", chat: data, receive_id: user,send_id:globalThis.user_phone,uid:globalThis.my_id }; let TXT_buf: chat_list = { mode: "txt", chat: data, receive_id: user,send_id:'1234'}; this.send_once(JSON.stringify(TXT_buf)); } static Send_IMG(user: String, data: String) { // @ts-ignore let IMG_buf: chat_list = { mode: "img", img: data, receive_id: user,send_id:"1234",uid:"1234" }; //let IMG_buf: chat_list = { mode: "img", img: data, receive_id: user,send_id:"1234"}; this.send_once(JSON.stringify(IMG_buf)); console.info(`IMG:${JSON.stringify(IMG_buf)}`) } static Send_MICC(user: String, data: String) { // @ts-ignore let MIC_buf: chat_list = { mode: "mic", mic: data, receive_id: user,send_id:"1234" }; this.send_once(JSON.stringify(MIC_buf)); console.info(`IMG:${JSON.stringify(MIC_buf)}`) } static Scan_user(user: String) { // @ts-ignore let IMG_buf: chat_list = { mode: "scan", user: user }; this.send_once(JSON.stringify(IMG_buf)); } static Add_user(user: String) { let phone = user.split('-')[1] // @ts-ignore let Add_buf: chat_list = { mode: "add", user: phone }; this.send_once(JSON.stringify(Add_buf)); } } ``` 其中涉及到数据json化解析和封装,在Message中主要是对接收数据的处理,比如说发送一次字符串 ```js {"mode":"img","img":"1701676163108","receive_id":"user","send_id":"123123"} ``` 需要按照mode和属性依次进行解析 ## (11)AI数据接入POST 这里主要使用到了POST提交的方式,基础例程如下: ```ts let request = http.createHttp() request.request( // 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定 "https://openai.api2d.net/v1/chat/completions", { method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET // 开发者根据自身业务需要添加header字段 header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ******' }, // 当使用POST请求时此字段用于传递内容 expectDataType: http.HttpDataType.OBJECT, // 可选,指定返回数据的类型 extraData: { "model": "gpt-3.5-turbo", "messages": [{ "role": "user", "content": `模拟:你是一名医生,我生病了,我对你说《${this.message}》,请直接回答` }] }, //usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定 }, (err, data) => { if (!err) { // data.result为HTTP响应内容,可根据业务需要进行解析 console.info('Result:' + JSON.stringify(data.result)); console.info('code:' + JSON.stringify(data.responseCode)); // data.header为HTTP响应头,可根据业务需要进行解析 console.info('header:' + JSON.stringify(data.header)); console.info('cookies:' + JSON.stringify(data.cookies)); // 8+ // 取消订阅HTTP响应头事件 request.off('headersReceive'); // 当该请求使用完毕时,调用destroy方法主动销毁 request.destroy(); // @ts-ignore const message_ai: string = data.result.choices[0].message.content } else { console.info('error:' + JSON.stringify(err)); // 取消订阅HTTP响应头事件 request.off('headersReceive'); // 当该请求使用完毕时,调用destroy方法主动销毁。 request.destroy(); } }) ``` 其本质是POST方式的提交,按照后端的协议,输入提交地址、header数据等等,即可实现返回值 # 五、设备端设计[Hi3861] ## (1)WEB配网 ![](Image/14.png) 本章主要讲述如何实现web配网,是在STA模式下,模拟为一个网站服务器,当手机或其它设备进行访问时,检测是否为浏览器的协议头(HTTP),返回一个封装好的网页界面,通过网页上输入框的填写实现配网。 ### HTTP协议介绍: 1. http协议->超文本传输协议 2. 应用:编写基于http协议的数据传输程序(网站中浏览器端获取网页的过程) 3. http请求作用:将要获取的内容以http协议的格式发送给服务端,服务端根据格式进行解析获取到其真实内容,将结果以http协议的格式回复给客户端 ```html 程皖配网
欢迎使用程皖配网

WiFi名称:

WiFi密码:

服务器IP:

服务器端口:

``` ![](Image/15.png) 实现的效果如下: ![](Image/16.png) ## (2)soft模式下实现网页服务器 该部分步骤分为四步:打开WIFI、进入softap模式,创建tcp服务器,解析HTTP指令。此处可参照 润和开源项目: https://gitee.com/hihopeorg/HarmonyOS-IoT-Application-Development/tree/master #### 1)打开WIFI ```c# 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模式 ```c 在softap.c文件下WifiAPTask函数,注册回调    //注册wifi事件的回调函数     g_wifiEventHandler.OnHotspotStaJoin = OnHotspotStaJoinHandler;     g_wifiEventHandler.OnHotspotStaLeave = OnHotspotStaLeaveHandler;     g_wifiEventHandler.OnHotspotStateChanged = OnHotspotStateChangedHandler;     error = RegisterWifiEvent(&g_wifiEventHandler); ``` #### 3)创建socket通道后进入判断接受内容循环 ```c  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 0) { #ifdef WRITE_BY_INT OLED_ShowChar(50,0,(unsigned char)(uart_buff_ptr[3] / 10 % 10 + '0'),16); OLED_ShowChar(58,0,(unsigned char)(uart_buff_ptr[3] % 10 + '0'),16); OLED_ShowChar(66,0,(unsigned char)(uart_buff_ptr[4] / 10 % 10 + '0'),16); OLED_ShowChar(74,0,(unsigned char)(uart_buff_ptr[4] % 10 + '0'),16); GET_PM25 = uart_buff_ptr[3]*100 + uart_buff_ptr[4]; OLED_ShowChar(40,40,(unsigned char)(GET_H / 10 % 10 + '0'),16); OLED_ShowChar(48,40,(unsigned char)(GET_H % 10 + '0'),16); OLED_ShowChar(40,20,(unsigned char)(GET_T / 10 % 10 + '0'),16); OLED_ShowChar(48,20,(unsigned char)(GET_T % 10 + '0'),16); OLED_Refresh(); #else hi_uart_write_immediately(DEMO_UART_NUM, uart_buff_ptr, len); #endif } else { printf("Read nothing!\n"); hi_sleep(1000); /* sleep 1000ms */ } } hi_task_delete(g_uart_demo_task_id); g_uart_demo_task_id = 0; return HI_NULL; } hi_void uart_demo(hi_void) { hi_u32 ret; hi_uart_attribute uart_attr = { .baud_rate = 38400, /* baud_rate: 115200 */ .data_bits = 8, /* data_bits: 8bits */ .stop_bits = 1, .parity = 0, }; /* Initialize uart driver */ ret = hi_uart_init(DEMO_UART_NUM, &uart_attr, HI_NULL); if (ret != HI_ERR_SUCCESS) { printf("Failed to init uart! Err code = %d\n", ret); return; } /* Create a task to handle uart communication */ hi_task_attr attr = {0}; attr.stack_size = UART_DEMO_TASK_STAK_SIZE; attr.task_prio = UART_DEMO_TASK_PRIORITY; attr.task_name = (hi_char*)"uart_demo"; ret = hi_task_create(&g_uart_demo_task_id, &attr, uart_demo_task, HI_NULL); if (ret != HI_ERR_SUCCESS) { printf("Falied to create uart demo task!\n"); } } ``` ## (5)DHT11传感器驱动 ![](Image/20.png) DHT11 数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,内部由一个 8 位单片机控制一个电阻式感湿元件和一个 NTC 测温元件。DHT11 虽然也是采用单总线协议,但是该协议与 DS18B20 的单总线协议稍微有些不同之处。 相比于 DS18B20 只能测量温度,DHT11 既能检测温度又能检测湿度,不过 DHT11 的精度和测量范围都要低于 DS18B20,其温度测量范围为 0~50℃,误差在±2℃;湿度的测量范围为 20%~90%RH(Relative Humidity 相对湿度—指空气中水汽压与饱和水汽压的百分比),误差在±5%RH。DHT11 电路很简单,只需要将 Dout 引脚连接单片机的一个 I/O 即可,不过该引脚需要上拉一个 5K 的电阻,DHT11 的供电电压为 3~5.5V。 因为使用的是单总线协议,其驱动程序如下: ```c /**************************************** 设置端口为输出 *****************************************/ void DHT11_IO_OUT(void) { //设置GPIO_11为输出模式 GpioSetDir(DHT11_GPIO, HI_GPIO_DIR_OUT); } //等待DHT11的回应 //返回1:未检测到DHT11的存在 //返回0:存在 u8 DHT11_Check(void) { hi_gpio_value gpio_val; u8 retry=0; GpioSetDir(DHT11_GPIO, HI_GPIO_DIR_IN);//配置为输入模式 hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val); while (gpio_val&&retry<100)//DHT11会拉低40~80us { hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val); retry++; hi_udelay(1); }; if(retry>=100)return 1; else retry=0; hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val); while ((!gpio_val)&&retry<100)//DHT11拉低后会再次拉高40~80us { hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val); retry++; hi_udelay(1); }; if(retry>=100)return 1; return 0; } //初始化DHT11的IO口 DQ 同时检测DHT11的存在 //返回1:不存在 //返回0:存在 u8 DHT11_Init(void) { //设置GPIO_11的复用功能为普通GPIO IoSetFunc(DHT11_GPIO, HI_IO_FUNC_GPIO_0_GPIO); //设置GPIO_11为输出模式 GpioSetDir(DHT11_GPIO, HI_GPIO_DIR_OUT); //设置GPIO_11输出高电平 GpioSetOutputVal(DHT11_GPIO, 1); DHT11_Rst(); //复位DHT11 return DHT11_Check();//等待DHT11的回应 } //复位DHT11 void DHT11_Rst(void) { DHT11_IO_OUT(); //SET OUTPUT DHT11_DQ_OUT_Low; //拉低DQ hi_udelay(20000);//拉低至少18ms DHT11_DQ_OUT_High; //DQ=1 hi_udelay(35); //主机拉高20~40us } //从DHT11读取一个位 //返回值:1/0 u8 DHT11_Read_Bit(void) { hi_gpio_value gpio_val; u8 retry=0; hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val); while(gpio_val&&retry<100){//等待变为低电平 hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val); retry++; hi_udelay(1); } retry=0; while((!gpio_val)&&retry<100){//等待变高电平 hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val); retry++; hi_udelay(1); } hi_udelay(40);//等待40us //用于判断高低电平,即数据1或0 hi_gpio_get_input_val(HI_IO_NAME_GPIO_0, &gpio_val); if(gpio_val)return 1; else return 0; } //从DHT11读取一个字节 //返回值:读到的数据 u8 DHT11_Read_Byte(void) { u8 i,dat; dat=0; for (i=0;i<8;i++) { dat<<=1; dat|=DHT11_Read_Bit(); } return dat; } //从DHT11读取一次数据 //temp:温度值(范围:0~50°) //humi:湿度值(范围:20%~90%) //返回值:0,正常;1,读取失败 u8 DHT11_Read_Data() { u8 buf[5]={ 0 }; u8 i; DHT11_Rst(); if(DHT11_Check()==0) { for(i=0;i<5;i++)//读取40位数据 { buf[i]=DHT11_Read_Byte(); } if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])//数据校验 { GET_H = buf[0]; GET_T = buf[2]; } }else return 1; return 0; } ``` ## (6)水质测量驱动(ADC) ![](Image/21.png) 水质的测量选择使用TDS传感器,该传感器为ADC类型的传感器,需要采集传感器的线性电压,调用HI3861的ADC硬件。 ![](Image/22.png) ```c hi_gpio_set_dir(HI_GPIO_IDX_7, HI_GPIO_DIR_IN);//选择ADC3通道 ret = hi_adc_read(HI_ADC_CHANNEL_3, &get_adc_data, HI_ADC_EQU_MODEL_8, HI_ADC_CUR_BAIS_DEFAULT, 0xff); if (ret == HI_ERR_ADC_PARAMETER_WRONG) { printf("ADC TEST WRONG Channel SUCCESS!\n"); } ``` ## (7)血压测量驱动 ![](Image/23.png) 血压的测量选择使用便携式测量,在开发中已与电子血压仪行业标杆欧姆龙和传统水银血压仪进行比较,较为准确,可作为参考使用。 当前为使用第一阶段,与厂商(批量)第二阶段合作时可以得到更多的数据,可以当做一次小型的体检,如下图: ![](Image/24.png) 其驱动方式为USART驱动,协议如下: ![](Image/25.png) 通过对数据的截取和发送即可实现。 ## (8)体重测量(应变片+HX711驱动) 体重的核心传感器是使用的应变片 ![](Image/26.png) 核心原理是通过形变改变电阻的阻值,这个变化一般是线性关系,而通过阻值的关系可以计算出体重即物体的质量,很多商贩用的称重也是这种器件。 除了应变片之外我们还需要使用高精度的ADC,因为应变片形变引起的变化量是非常小的,必须使用高精度的ADC才能采集出来,而HI3861的自身ADC是10位即4096级别的,远不能满足我们的要求,所以选择了通用搭配的HX711来实现。 ![](https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsc02.alicdn.com%2Fkf%2FHTB1FkQKSXXXXXXbXpXX760XFXXXw%2FDual-Channel-HX711-Weighing-Pressure-Sensor-24.png&refer=http%3A%2F%2Fsc02.alicdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1663057360&t=0f1a5526209007334c771d425e866ab2) HX711是一款专为高精度电子秤而设计的24位A/D转换器芯片。与同类型其它芯片相比,该芯片集成了包括稳压电源、片内时钟振荡器等其它同类型芯片所需要的外围电路,具有集成度高、响应速度快、抗干扰性强等优点。降低了电子秤的整机成本,提高了整机的性能和可靠性。该芯片与后端MCU 芯片的接口和编程非常简单,所有控制信号由管脚驱动,无需对芯片内部的寄存器编程。输入选择开关可任意选取通道A 或通道B,与其内部的低噪声可编程放大器相连。通道A 的可编程增益为128 或64,对应的满额度差分输入信号幅值分别为±20mV或±40mV。通道B 则为固定的32 增益,用于系统参数检测。芯片内提供的稳压电源可以直接向外部传感器和芯片内的A/D 转换器提供电源,系统板上无需另外的模拟电源。 通过描述可以得知HX711是使用SCL+DO的形式实现的,即一根为数据线一根为信号线,在HI3861的驱动程序如下: ```c 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); } ``` ## (9)TCP数据发送和接收 因为HI3861的线程限制,这边使用双线程,一个实现TCP数据的发送,另一个实现TCP数据的接收 #### 发送线程: ```c 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); } ``` #### 接收处理线程: ```c 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"); } } ``` # 六、注意事项 * #### 在线医生运行聊天功能,需要启用后端服务器(局域网或者互联网) * #### 打开python目录,修改the_ip的数值为当前服务器ip,运行main.py即可 * #### 同时在【在线医生】的元服务目录中也需要修改服务器的ip 在NetService.ets中同样也是the_ip变量,将其修改为服务器ip * #### 家庭医生整体程序都进行了上传,包括家庭医生终端(APP)、运动健康(元服务)、在线医生(元服务)、python(服务器简易后端)、hi3861驱动程序源码,本系统包括这五个部分,在上述文件夹内是完整工程,可直接进行编译和安装