1. 项目背景与整体方案设计
1.1 为什么是Flutter for OpenHarmony
这两年OpenHarmony生态的进展比我预想中快得多。早先大家还在争论要不要入坑,现在不少团队已经把手伸向了智能家居、工业平板、医疗设备这些面向垂直行业的场景。我做智慧养老App的选型调研时,对比了几条路线:纯OpenHarmony原生开发、Web套壳、Flutter跨平台,最后敲定Flutter for OpenHarmony这条思路。
原因很直接。第一,团队里有现成的Flutter基础,Dart语言上手成本低;第二,Flutter在UI渲染一致性上做得相当好,Pixel级别的控制力对养老场景里的大字体、高对比度适配很重要;第三,OpenHarmony目前的应用生态还不够丰富,原生组件和第三方库的中长期维护存在不确定性,而Flutter有庞大的插件生态,可以平滑迁移一部分能力到OpenHarmony上。说白了,选Flutter不是因为它比ArkUI更先进,而是因为它能让我们在保留跨平台能力的同时,更快地把核心业务跑起来,降低试错成本。
这个项目里的核心功能是心率监测。养老院场景下,老人佩戴心率手环或使用床边心率设备,数据通过蓝牙或USB传到搭载OpenHarmony的平板或电视盒子,App实时展示心率波形、数值,并在异常时报警推送给护工和家属。整个链路里,数据采集、信号处理、异常判断、UI渲染、后端上报,每个环节都有值得展开讲的坑。
1.2 智慧养老场景的特殊需求
养老App和普通健康App有个本质区别:使用者不一定是一线用户。老人可能不会操作,甚至意识不到设备异常,真正紧盯屏幕的是护工和家属。所以产品设计上,我做了三层角色的拆分:老人只看大数字和醒目颜色,护工看波形和趋势,家属远程接收异常通知。
对应到技术实现上,有几个硬性要求:
- 心率数值刷新延迟不能超过2秒,否则护工无法实时判断老人的状态
- 波形平滑度要足够好,不能因为数据抖动产生大量误报警
- 字体和对比度要支持无障碍模式,OpenHarmony上要适配系统的fontScale设置
- 设备要7x24小时运行,不能频繁崩溃或内存泄漏,这对长时间挂在后台的采集服务是个考验
这些需求框定了技术选型和架构设计的方向。心率监测不是简单画个折线图就完事,它牵涉到从硬件协议解析到UI渲染再到云端推送的完整链路,任何一个环节掉链子,整套系统就不可用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建与工程创建
2.1 OpenHarmony SDK与Flutter版本搭配
先说说环境。OpenHarmony的版本迭代很快,Flutter对OpenHarmony的支持也是最近几个版本才逐步稳定的。我使用的组合是OpenHarmony 4.0 Release + Flutter 3.7.12(OpenHarmony分支),这个搭配在社区里验证比较多,踩坑资料相对齐全。
OpenHarmony SDK的下载和安装这里就不赘述了,重点说配置环节的坑。OpenHarmony的SDK不像Android SDK那样装完就完事,它需要特别指定Native API版本,否则编译时会出现签名或接口不匹配的问题。我遇到的一个典型报错是:
bash复制Native API version '10' is not compatible with SDK version '4.0.0(11)'
这个问题的根源是项目里配置的native api版本和SDK实际版本不一致。解决办法是检查build-profile.json5文件,把runtimeOS和apiVersion对齐到SDK配套的版本。
Flutter for OpenHarmony的安装流程和标准Flutter略有不同,需要从OpenHarmony的镜像仓库拉取特定分支:
bash复制git clone -b openharmony-3.7 https://gitee.com/openharmony-sig/flutter_flutter.git
克隆完成后,把bin目录添加到PATH环境变量。这里有个细节:不要覆盖你本机已有的标准Flutter,建议用别名或独立目录管理,否则来回切项目会疯掉。
2.2 创建Flutter工程并配置OpenHarmony平台
创建工程和标准Flutter一致,用flutter create即可。但创建完成后需要手工添加OpenHarmony平台的适配目录,这一步官方没有完全自动化,需要从模板工程里拷贝ohos目录。
我整理一下关键步骤:
- 创建一个普通Flutter工程:
flutter create elder_heart - 从社区模板或官方示例中拷贝
ohos目录到工程根目录 - 修改
ohos/entry/src/main/module.json5,配置应用包名、权限声明 - 在
ohos/build-profile.json5中配置签名信息 - 用
flutter build hap --debug或--release构建
关于签名,OpenHarmony的调试签名走的是自动签名,在DevEco Studio里配置比较方便。但如果你像我一样习惯命令行构建,需要手动生成p12和cer文件,然后在build-profile.json5里引用。
有一点特别提醒:OpenHarmony的module.json5里声明权限和Android的AndroidManifest.xml逻辑不同,它需要在requestPermissions数组里逐项声明,而且部分权限涉及用户授权弹窗的逻辑,需要在代码里动态申请。后面讲心率监测时会细说。
2.3 运行到真机或模拟器的常见坑
OpenHarmony的模拟器对USB外设的支持比较弱,心率监测这种涉及硬件交互的项目,我建议直接上真机调试。RK3568开发板是个性价比不错的选择,几百块钱就能拿到,跑OpenHarmony 4.0绰绰有余。
真机调试连接时,先执行hdc list targets确认设备在线。hdc是OpenHarmony的设备连接工具,类似Android的adb。如果设备列表为空,大概率是驱动问题或hdc版本不匹配,重装配套驱动一般能解决。
安装App用hdc install,日志查看用hdc hilog。这个和Android的adb logcat类似,但有一个坑:hilog默认会刷出大量系统日志,需要做过滤:
bash复制hdc hilog | grep -i "flutter\|elder_heart\|heartrate"
跑起来之后如果遇到白屏,不要急着怀疑代码,先检查module.json5里的入口ability是否配置正确。Flutter for OpenHarmony的入口能力映射和标准Flutter不一样,如果abilities配置里的srcEntry路径不对,应用启动后只会看到一个空白窗口。
3. 心率监测原理与数据采集实现
3.1 心率传感器的数据从哪里来
心率监测的硬件方案,市面上主流的有两种:光电容积脉搏波(PPG)和心电(ECG)。PPG方案成本低、佩戴方便,手环和手表基本都是这种方案;ECG方案精度更高,但需要电极贴片,适合医疗级场景。我的项目里用的是PPG方案的蓝牙手环,数据通过BLE协议上报。
PPG的原理不复杂。血液对光的吸收能力随心脏搏动周期性变化,传感器里的LED发出绿光或红光,光电二极管接收反射光,输出一个随心跳波动的电信号。这个信号的频率就是心率。但问题在于,这个原始信号非常脏,里面混着呼吸扰动、肢体运动伪迹、环境光干扰,甚至传感器贴合松紧都会影响波形质量。
所以数据采集不只是把蓝牙数据读出来那么简单,必须经过信号预处理。先做带通滤波(通常用0.5Hz到5Hz的带通),再做平滑去噪,然后才轮到心率计算。滤波器的实现方式有很多,我推荐直接用Dart实现一个简单的IIR滤波器,虽然比不上MATLAB里的复杂算法,但对养老场景的静息心率监测来说完全够用,还能省去调用原生代码的工程复杂度。
3.2 OpenHarmony的BLE通信实现
蓝牙通信是数据链路的核心。OpenHarmony的BLE API和Android的BluetoothGatt类似,但细节上有些差异。我用的是@ohos.bluetooth.ble这个模块。
先看权限配置。module.json5里需要声明:
json复制{
"name": "ohos.permission.USE_BLUETOOTH",
"reason": "用于连接心率监测设备",
"usedScene": {
"abilities": ["EntryAbility"]
}
}
注意,OpenHarmony的蓝牙权限还包括ACCESS_BLUETOOTH和MANAGE_BLUETOOTH,具体取决于API级别。调试时如果发现扫描不到设备,先检查权限是否都在requestPermissions里声明了。
扫描设备的代码大致如下:
dart复制import 'package:flutter/services.dart';
import 'package:flutter_ohos_ble/ble_controller.dart';
final BleController bleController = BleController();
// 初始化蓝牙
await bleController.init();
// 开始扫描
bleController.startScan().listen((device) {
if (device.name.contains('HeartRate')) {
// 找到心率设备,停止扫描并连接
bleController.stopScan();
bleController.connect(device.deviceId);
}
});
这里用的flutter_ohos_ble是一个社区维护的Flutter插件,封装了OpenHarmony的BLE能力。如果你项目里没有现成的插件,可以用Platform Channel自己封装,思路和在Android上做插件完全一致,只是原生侧的API从Android SDK换成了OpenHarmony SDK。
连接成功后,需要先发现服务,然后订阅心率特征值的通知。标准的心率服务UUID是0x180D,心率测量特征值是0x2A37,这个特征值会持续上报心率数据包,数据格式是固定的:第一个字节的高位表示心率格式(0表示UINT8,1表示UINT16),低位表示传感器是否接触;后面的字节是心率值,如果传感器还有其他数据(比如PPG波形),会按Flags中的配置继续解析。
我封装了一个数据解析函数:
dart复制List<int> heartRateFromBytes(ByteData data) {
final flags = data.getUint8(0);
final isUint16 = (flags & 0x01) == 0x01;
if (isUint16) {
return [data.getUint16(1, Endian.little)];
} else {
return [data.getUint8(1)];
}
}
这一段看起来简单,但实际开发中很容易踩坑:不同厂商的心率设备在数据格式上并不完全遵守蓝牙GATT规范标准,有的会额外加一个序列号字节,有的把PPG波形数据和心率值混在一起。所以协议解析这步一定要用真实设备验证,不能只看文档。
3.3 数据缓冲与丢失保护
蓝牙数据是流式的,每秒大概会收到25到100个数据点,取决于设备的上报频率。如果直接把数据丢给UI层,刷新频率太高会导致UI卡顿,太低又会丢失心跳细节。我在中间加了一个环形缓冲区,用定时器每500毫秒批量取一次数据。
dart复制class HeartRateBuffer {
final List<int> _buffer = [];
final int maxSize = 200;
void add(int value) {
_buffer.add(value);
if (_buffer.length > maxSize) {
_buffer.removeAt(0);
}
}
List<int> takeBatch() {
if (_buffer.isEmpty) return [];
final batch = List<int>.from(_buffer);
_buffer.clear();
return batch;
}
}
设计环形缓冲的关键是防止数据堆积。如果某次UI刷新卡顿超过一秒,缓冲区里可能积累了上百个数据点,这时候再全部绘制出来没有意义,反而会让波形看起来像一堵墙。我的策略是每次最多取最近80个点,超过部分直接丢弃,优先保证实时性。
真实场景中还有一种情况:蓝牙连接偶发断连,数据流中断几秒甚至几十秒。这时App不能直接把心率显示为0,否则会触发误报警。我加了一个超时保护逻辑:如果超过5秒没有收到新数据,就标记数据过期,UI显示"信号中断",而不是显示0。这个细节在养老场景里极其重要,护工看到0可能会慌乱,看到"信号中断"就会去检查设备,差异很大。
4. 心率算法与异常检测
4.1 基于峰值检测的心率计算
心率计算的核心是从波形中提取心跳频率。最常见的算法是峰值检测:找到波形中的R波或脉搏波主峰,计算相邻峰值的时间间隔,用60除以间隔得到瞬时心率。
峰值检测的复杂度在于怎么排除干扰。PPG信号里经常出现基线漂移,整体波形上下浮动,直接找最大值会误判。我的做法是先做差分运算,再用阈值判断。
具体流程是:
- 对输入序列做一阶差分
- 用滑动窗口计算局部均值和标准差,建立自适应阈值
- 从正跳变到负跳变的转折点中,筛选超过阈值的点作为候选峰值
- 对候选峰值做200毫秒的最小间隔约束,防止同一心跳被重复计数
自适应阈值的意义在于应对信号质量变化。老人情绪波动或轻微移动时,PPG信号幅度会变,固定阈值很容易失效。我用了一个简化的动态阈值:
dart复制double adaptiveThreshold(List<double> diffs) {
final mean = diffs.reduce((a, b) => a + b) / diffs.length;
final variance = diffs
.map((d) => (d - mean) * (d - mean))
.reduce((a, b) => a + b) / diffs.length;
final stdDev = sqrt(variance);
return mean + 1.5 * stdDev;
}
这个算法在静息状态下准确率还不错,但运动状态下误差会明显增大。如果以后要扩展运动场景,建议换成基于FFT的频域心率估计算法,或者引入加速度传感器做运动伪迹消除,这里先不展开。
4.2 心率异常的判断逻辑
异常检测不能只看一个数值,要看趋势。心率80对于普通人正常,但对一个静息心率常年60的老年人来说,突然升到80可能就有问题。所以我的异常判断逻辑是双层设计:
第一层是硬阈值判断,心率低于50或高于120直接报警,这是比较保守的医疗参考范围;第二层是趋势判断,连续30秒内心率变化超过30次/分钟,或出现持续10秒以上的不规则波动,会触发预警。
这里要特别注意报警的有效期和去重。如果只是心率瞬时冲到125又马上回落,不需要通知家属,否则会把人吓死。我的逻辑是:触发异常后,必须持续观察15秒,确认异常持续存在才真正生成报警事件。这个"确认窗口"的时长可以根据场景调节,养老院场景建议在15到30秒之间。
异常事件的数据结构我定义成JSON,方便统一上报:
json复制{
"type": "tachycardia",
"heartRate": 128,
"duration": 20,
"time": "2024-06-01 08:12:30"
}
5. 核心功能实现与UI设计
5.1 心率波形图绘制
Flutter的CustomPaint是画波形图的利器,性能和灵活性都不错。我在实现时没有用第三方图表库,原因很简单:心率波形需要高频刷新,第三方库为了功能全面往往牺牲了渲染性能,自己用CustomPaint可以针对高频更新做优化。
核心思路是维护一个数值列表,每500毫秒把新数据追加进去,然后用CustomPainter绘制。绘制时只画最近800个点,形成滚动效果。
dart复制class HeartWavePainter extends CustomPainter {
final List<double> data;
final double minY;
final double maxY;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.green
..strokeWidth = 2
..style = PaintingStyle.stroke;
final path = Path();
if (data.isEmpty) return;
final stepX = size.width / 800;
final rangeY = (maxY - minY).clamp(1.0, double.infinity);
for (int i = 0; i < data.length; i++) {
final x = i * stepX;
final y = size.height - (data[i] - minY) / rangeY * size.height;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant HeartWavePainter oldDelegate) {
return oldDelegate.data != data;
}
}
关于刷新性能,有一个优化点:不要用setState刷新整个页面,而是用ValueNotifier配合ValueListenableBuilder只刷新波形区域。否则整个页面包括卡片、按钮都会跟着重建,在低配RK3568上会出现明显掉帧。
另外,波形颜色也有讲究。我在项目里用了三层颜色:绿色表示心率正常区,黄色表示临界区(心率在50-60或100-120),红色表示报警区。这样护工扫一眼屏幕就能判断老人的状态,不需要看数字。
5.2 养老场景的UI适老化设计
适老化设计不是简单地把字体调大。我在实际开发中总结了几个要点:
一是信息层级要极简。主界面只显示三个核心元素:当前心率数值、波形图、状态指示灯。其他所有功能(历史记录、设置、设备管理)都收进抽屉菜单,二级入口尽量不占用首屏空间,避免老人或护工被过多信息干扰。
二是高对比度配色。文字不要用浅灰色,统一用纯黑或深绿色;背景用白色或浅米色。这个对比度标准可以参考无障碍设计的WCAG AA级别要求,前景和背景的对比度至少4.5:1。
三是触控区域要够大。核心操作按钮的尺寸至少56x56像素,相邻按钮间距不小于8像素,防止误触。这个在平板设备上相对好实现,但要特别注意系统字体缩放后布局是否溢出。
四是支持系统级字体缩放。OpenHarmony的系统设置里可以调整字体大小,App必须适配不同档位的fontScale,而不是固定字号。我建议用MediaQuery.textScalerOf(context)来动态计算字号,而不是硬编码px。
5.3 后台运行与保活
养老院场景下,App需要长时间运行,不能因为屏幕休眠就断开蓝牙或停止心率采集。我在这块踩了不少坑,最终方案是分三层:
第一层,在module.json5中声明backgroundModes,把蓝牙和网络相关的能力加进去。这个配置类似Android的foregroundServiceType,告诉系统这个应用需要在后台运行蓝牙服务。
第二层,在代码里合理使用OpenHarmony的reminderAgentManager或continuousTask能力申请后台任务。但坦白说,OpenHarmony的后台机制还在演进中,不同版本表现不一致,建议测试时重点验证。
第三层,最稳妥的方案:在设置里引导用户配置"电池优化白名单"。这个引导流程要从App内部跳转到系统设置页面,OpenHarmony上有对应的@ohos.settings接口可以操作。
屏幕常亮也很重要。我在项目里用了window.setKeepScreenOn(true)来保持屏幕常亮,但这里有个矛盾:长时间常亮会加速OLED屏幕烧屏,而且耗电快。最后我采取了折中方案:心率异常或设备连接状态异常时强制点亮屏幕并保持常亮,正常状态下允许屏幕进入休眠但保持采集服务运行。
6. 数据存储与家属端联动
6.1 本地历史数据存储
心率数据除了要实时显示,还要存历史记录,方便医生或家属查看趋势。存储方案我用了@ohos.data.relationalStore,这是OpenHarmony自带的轻量级关系型数据库,性能和SQLite差不多。
建表语句很简单:
sql复制CREATE TABLE IF NOT EXISTS heart_rate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
heart_rate INTEGER NOT NULL,
signal_quality INTEGER NOT NULL
);
写入策略上要注意一个现实问题:如果每秒钟存一条记录,一天会产生86400条数据,三个月就是将近800万条。对平板设备的存储来说不算大,但查询时如果没建索引,会明显变慢。我加了timestamp索引,并且只在心率变化超过1bpm时才写入,对于平稳心率,把多条记录合并成一条区间记录。这样既保留了趋势信息,又大幅减少了数据量。
6.2 异常事件上报与家属通知
心率异常一旦确认,需要推送给护工和家属。完整链路是:OpenHarmony App通过HTTP接口调用云端服务,云端服务再触发推送通知。这里用dio做HTTP客户端比较顺手,底层走的是OpenHarmony的网络API。
我把异常上报设计成异步任务,不能阻塞UI线程。Dart的异步模型处理这个很自然:
dart复制Future<void> reportAnomaly(AnomalyEvent event) async {
try {
final response = await dio.post(
'https://api.example.com/anomaly',
data: jsonEncode(event.toJson()),
);
if (response.statusCode == 200) {
// 上报成功
} else {
// 上报失败,进入重试队列
retryQueue.add(event);
}
} catch (e) {
// 网络异常,进入重试队列
retryQueue.add(event);
}
}
重试队列必须设计成有上限的,否则网络长时间不可用时内存会持续膨胀。我用了容量100的队列,超出部分直接丢弃,因为心跳异常这个场景下,旧的告警过时了,新的告警才有意义。
家属端的通知形态可以是App推送、短信或微信模板消息。OpenHarmony自己的推送服务@ohos.push目前还在完善中,我建议优先接云端厂商的推送通道,比如个推、极光这些支持多端的方案,或者直接走WebSocket长连给自己开发的小程序发消息。
7. 常见问题与排查技巧实录
7.1 BLE设备扫描不到或频繁断连
这个问题排在所有疑难杂症的第一名。我分享几个排查方向:
先确认权限。OpenHarmony的蓝牙权限和定位权限在某些版本上是绑定的,如果只申请了蓝牙权限没申请定位权限,扫描结果可能为空。这个和Android 12及以上的行为类似。
再确认服务发现时机。有些设备连接成功后不能马上GATT discover,需要等待100到200毫秒,否则会发现不了服务。我加了一个延时:
dart复制await bleController.connect(deviceId);
await Future.delayed(const Duration(milliseconds: 200));
await bleController.discoverServices();
如果还是断连频繁,大概率是设备的蓝牙栈和OpenHarmony的兼容性问题。这个很难通过代码完全解决,建议直接换设备。我在项目里同时适配了三款手环,最后主推的是兼容性最好的一款,其余两款作为备选,通过配置中心切换。
7.2 UI刷新卡顿与内存占用飙升
心率波形一秒钟要刷新2次,每次画800个点,如果在低端设备上还开了全屏泛光效果,卡顿几乎是必然的。
我的排查工具是DevEco Studio自带的Profile工具。刚开始发现内存持续上涨,定位了半天才发现是CustomPainter的shouldRepaint返回了true,导致每次build都重建Painter对象。改成比较数据版本号后,内存曲线立刻平稳了。
还有一个常见的坑:Timer.periodic的定时器在页面销毁后没有取消,导致后台持续刷新UI。这个问题在养老App这种长时间运行的场景里尤其致命,内存泄漏会越积累越严重。我强制要求所有Stream和Timer在dispose里释放,并做了LeakChecker在Debug模式下检测。
7.3 Flutter插件在OpenHarmony上的兼容性
这是目前最大的现实限制。很多通用Flutter插件还没有适配OpenHarmony,或者适配了但功能不完整。我在项目中遇到的一个典型问题:官方Flutter项目的shared_preferences插件在OpenHarmony上官方适配晚于预期,需要从OpenHarmony-SIG的仓库拉取替代版本才能编译通过。
解决办法是改用OpenHarmony的自有能力。以本地键值对存储为例,直接用@ohos.data.preferences封装一个Dart层,性能更优,还省去了插件依赖:
dart复制import 'package:flutter/services.dart';
class OhosPreferences {
static const MethodChannel _channel = MethodChannel('elder_heart/preferences');
static Future<void> saveString(String key, String value) async {
await _channel.invokeMethod('saveString', {'key': key, 'value': value});
}
static Future<String?> getString(String key) async {
return await _channel.invokeMethod('getString', {'key': key});
}
}
所以我的建议是:不要迷信Flutter插件的跨平台承诺,凡是涉及OpenHarmony系统能力的(蓝牙、后台任务、数据库、通知),先查一下插件是否适配,再决定是用插件还是用Platform Channel自己写。多花点时间研究系统API反而是最可控的路径。
7.4 常见问题速查表
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 应用无法安装到设备 | hap包签名未做 | 用hdc install查看具体报错,配置签名信息 |
| 应用启动白屏 | entry ability配置错误 | 检查module.json5中的入口路径 |
| 蓝牙扫描不到设备 | 缺少定位/蓝牙权限 | 在requestPermissions中补全权限声明 |
| 心率值跳动幅度大 | 传感器贴合不紧或运动伪迹 | 带通滤波参数调整,或要求用户紧佩戴设备 |
| 波形绘制卡顿 | setState刷新整个页面 | 改用ValueNotifier局部刷新 |
| 后台运行被系统杀掉 | 未配置后台运行能力 | 申请backgroundModes,引导用户加入白名单 |
| 存储数据增长过快 | 每笔心跳都落库 | 增加变化阈值,合并平稳区间记录 |
8. 后续扩展方向
心率监测只是智慧养老App的第一块拼图。跑通这套"Flutter for OpenHarmony + 传感器 + 异常上报"的架构之后,扩展血氧、体温、跌倒检测这些功能,流程是高度类似的。硬件协议解析、数据缓冲、UI展示、云端上报这些模块都可以复用,只需要替换传感器数据的解析逻辑。
我目前正在做的一个扩展是睡眠监测。心率数据加上加速度计数据,可以判断老人的睡眠阶段,包括清醒、浅睡、深睡。这部分算法还在验证中,但架构上完全兼容现有的数据链路。
另外,有个方向特别值得关注:OpenHarmony的分布式能力。如果老人家里有多台OpenHarmony设备(卧室平板、客厅电视盒子、厨房智能屏),可以利用分布式软总线把心率数据流转到当前有人的设备上,这样护工或家属在任何房间都能看到实时状态,不需要固定在一台设备前。
这个项目的后续迭代我会继续用Flutter for OpenHarmony,一方面是团队的技术栈积累已经在这条路线上,另一方面是跨平台能力确实降低了适配成本。如果你也在做OpenHarmony上的健康类应用,欢迎交流,这还是一个非常早期但潜力很大的领域,大家一起趟坑能少走很多弯路。
