基于鸿蒙OS开发静脉输液智能监控系统(11)-通知推送与语音提醒服务
基于鸿蒙OS开发静脉输液智能监控系统(11)-通知推送与语音提醒服务
目录
- 1. 移动端提醒系统设计原则
- 2. NotificationService设计
- 3. SpeechService设计
- 4. 提醒话术设计
- 5. 提醒触发时机
- 6. 振动提醒
- 7. 三角色提醒差异化
- 8. 未来:跨设备推送
1. 移动端提醒系统设计原则
在医疗物联网场景中,提醒系统的设计直接关系到患者安全和医护效率。IVGuard项目作为智能输液监控系统的移动端组件,其提醒子系统必须遵循以下四大核心设计原则,以确保在关键时刻信息能够准确、及时、有效地传达给相关用户。
1.1 及时性原则
核心理念:液位低于阈值立即提醒,不延迟
输液监控的核心价值在于"早发现、早处理"。传统输液监控完全依赖人工巡视,护士需要定时逐床检查,这在患者数量多、护理人员有限的情况下极易出现疏漏。IVGuard的提醒系统将"及时性"作为第一优先级,其设计要点包括:
- 零延迟触发:当系统检测到液位低于预设阈值时,提醒逻辑在当前轮询周期内立即执行,不做任何延迟处理。与某些应用采用"批量通知"或"延迟聚合"的策略不同,医疗场景中每秒的延迟都可能影响患者安全,因此IVGuard采用即时触发机制。
- 3秒轮询周期:MonitorPage中设置3秒一次的数据刷新周期,这意味着从液位变化到触发提醒的最长延迟不超过3秒。在蓝牙传输稳定的前提下,这个延迟在临床场景中是完全可以接受的。
- 优先级调度:提醒任务的执行优先级高于普通UI刷新。当系统资源紧张时,数据采集和提醒判断逻辑应当获得优先执行权,确保关键预警不被其他非关键任务阻塞。
- 无缓冲区设计:提醒消息不经过消息队列缓冲,而是直接触发通知栏发布和语音播放。这种"直通"设计虽然增加了系统调用频率,但在医疗场景下保障了信息的即时到达。
- 初始化即检查:页面加载完成后立即执行一次阈值检查,不等待首个轮询周期。这避免了用户刚进入页面时出现的"盲区"时段。
在实际实现中,及时性还需要考虑以下边界情况:网络波动导致的数据延迟上报、蓝牙断连后的重连提醒、以及应用从后台恢复到前台时的状态同步检查。这些场景下的及时性保障将在后续章节详细讨论。
及时性的量化指标也是系统验收的重要标准。IVGuard的及时性指标定义如下:
| 指标 | 目标值 | 测量方法 |
|---|---|---|
| 液位变化到触发提醒 | ≤ 3秒 | 从BLE数据包到达时间到通知发布时间 |
| 通知栏弹出到用户可见 | ≤ 1秒 | 系统通知服务处理时间 |
| 语音播报启动延迟 | ≤ 500ms | 从调用play到实际发声时间 |
| 振动触发延迟 | ≤ 100ms | 从调用vibrate到振动马达启动 |
这些指标将在系统测试阶段通过自动化测试用例进行验证,确保每个版本都满足及时性要求。
1.2 多通道原则
核心理念:通知栏+语音+振动,三重保障
单一提醒通道存在天然的不可靠性。用户可能处于嘈杂环境听不到语音提示,可能将手机放在口袋中看不到通知栏,也可能在会议中关闭了振动功能。IVGuard采用三通道并行提醒策略,确保信息能够通过至少一条通道触达用户:
| 通道 | 优势 | 局限 | 适用场景 |
|---|---|---|---|
| 通知栏(Notification) | 持久可见、可回顾、不打扰他人 | 需用户主动查看 | 所有角色、所有场景 |
| 语音(Speech) | 即时感知、无需主动查看 | 噪音环境失效、打扰他人 | 患者端近距离场景 |
| 振动(Vibration) | 触觉感知、隐蔽性好 | 手机不在身上无效 | 随身携带场景 |
三通道的设计遵循"独立触发、协同提醒"的理念:
- 独立触发:每个通道的触发逻辑相互独立,一个通道的失败不影响其他通道的执行。例如,语音播放失败时,通知栏和振动仍然正常触发。这种独立性在代码层面体现为三个服务的调用之间没有依赖关系,任何一个调用的异常都不会阻止后续调用的执行。
- 协同提醒:同一预警事件的三通道提醒在时间上尽可能同步,用户几乎同时感受到通知栏弹出、语音播报和振动反馈,形成多感官协同的提醒效果。这种协同不是通过同步等待实现的,而是通过并行触发实现的——三个通道同时发起调用,各自独立完成。
- 通道降级:当某个通道不可用(如语音被用户关闭、振动权限未授予)时,系统自动降级到可用通道组合,不产生错误或异常。降级是静默的,用户不会收到"语音不可用"之类的提示,因为可用通道已经在正常工作。
多通道架构的可靠性分析:
假设单通道的触达率为:
- 通知栏触达率:85%(用户可能忽略通知)
- 语音触达率:80%(噪音环境、距离限制)
- 振动触达率:75%(手机不在身上、静音模式)
三通道并行的综合触达率计算:
P(至少一个通道触达) = 1 - P(全部未触达) = 1 - (1-0.85) × (1-0.80) × (1-0.75) = 1 - 0.15 × 0.20 × 0.25 = 1 - 0.0075 = 99.25%
从85%的单通道触达率提升到99.25%的综合触达率,这就是多通道策略的核心价值。
1.3 分级原则
核心理念:info(蓝色)/warning(橙色)/danger(红色),不同级别不同响应
输液监控场景中的提醒并非都是紧急的。一次低液位预警和一次输液完成通知在紧急程度上有明显差异,如果所有提醒都采用相同的强度和方式,将导致"狼来了"效应——用户逐渐对提醒脱敏,忽视真正紧急的情况。IVGuard采用三级分类体系:
Level 1 — Info(信息级,蓝色标识)
- 触发条件:液位降至50%以下(可配置)
- 提醒方式:通知栏静默通知 + 短振动
- 语音播报:无
- 设计意图:信息告知,无需立即行动。用户可以在方便时查看详情。
- 通知样式:蓝色图标,标准通知栏样式,不弹出横幅
- 振动模式:短振动100ms
- 通知优先级:DEFAULT
信息级提醒的典型场景:输液进度过半,提醒用户关注剩余液量。这个级别的提醒不需要用户立即采取行动,只是提供一个信息参考。在临床场景中,护士通常会在巡视时查看此类通知,而不会中断当前工作。
Level 2 — Warning(预警级,橙色标识)
- 触发条件:液位降至20%以下(可配置warningThreshold)
- 提醒方式:通知栏横幅通知 + 语音播报 + 长振动
- 语音播报:“您的输液即将完成,请留意”
- 设计意图:需要关注,建议尽快处理。语音播报确保用户能够即时感知。
- 通知样式:橙色图标,横幅弹出,通知栏置顶
- 振动模式:长振动500ms
- 通知优先级:HIGH
预警级提醒是IVGuard最核心的提醒级别。当液位降至预设阈值以下时,意味着输液可能在30-60分钟内完成(具体取决于流速),需要护士安排处理时间。语音播报在这个级别启用,确保即使患者正在休息或注意力不在手机上,也能够通过听觉通道感知到提醒。
Level 3 — Danger(危险级,红色标识)
- 触发条件:液位降至0%(输液完成)或流速严重异常
- 提醒方式:通知栏全屏意图 + 语音循环播报 + 双振动
- 语音播报:“输液已完成,请通知护士”(循环2次)
- 设计意图:需要立即行动。全屏通知确保用户无法忽略。
- 通知样式:红色图标,全屏弹出(如果系统支持),持续振动
- 振动模式:双振动200ms+200ms
- 通知优先级:MAX
危险级提醒只在使用者必须立即行动的场景下触发。输液完成后如果不及时拔针,可能导致回血、空气栓塞等严重并发症。因此,这个级别的提醒采用了最强烈的提醒方式——全屏通知和循环语音播报,确保用户无法忽略。
分级系统的实现需要在NotificationRequest中携带不同的channel和priority参数,同时SpeechService和振动服务根据级别选择不同的播放模式和振动模式。这种分级设计确保了信息传达的精确性和用户体验的合理性。
分级与系统通知SlotType的映射关系:
| IVGuard级别 | 系统SlotType | 系统行为 |
|---|---|---|
| Info | SERVICE_INFORMATION | 静默通知,无声音无振动 |
| Warning | SOCIAL_COMMUNICATION | 弹出横幅,系统提示音 |
| Danger | SOCIAL_COMMUNICATION + FullScreenIntent | 全屏弹出,持续提醒 |
1.4 用户可控原则
核心理念:语音和振动可独立开关
不同使用场景下用户对提醒方式的需求差异很大。在医院病房中,夜间语音提醒可能影响其他患者休息;在门诊输液室中,振动提醒可能因为手机放在桌上而失去意义。IVGuard将提醒通道的控制权完全交给用户:
- 语音开关:
AppSettings.voiceAlertEnabled,控制SpeechService的全局启用/禁用 - 振动开关:
AppSettings.vibrationEnabled,控制振动提醒的全局启用/禁用 - 通知栏:始终开启,不可关闭。这是最低保障通道,确保用户至少能通过通知栏获取信息。
- 预警阈值:
AppSettings.warningThreshold,用户可自定义预警触发液位百分比
开关设置通过DataStore持久化存储,应用重启后设置不丢失。设置界面提供清晰的开关描述和即时预览功能,帮助用户理解每个开关的作用。所有设置变更立即生效,无需重启应用。
用户可控原则的扩展设计:
- 夜间模式:自动在设定时间段(如22:00-07:00)关闭语音和振动,仅保留通知栏
- 勿扰模式联动:当系统勿扰模式开启时,自动降级提醒强度
- 按场景配置:门诊模式(安静环境,语音关闭)vs 病房模式(允许语音)
- 测试功能:设置页面提供"测试提醒"按钮,用户可以模拟触发各类提醒以验证当前配置
这些扩展功能将在后续版本中逐步实现,当前版本先确保基础的开关控制功能稳定可用。
2. NotificationService设计
NotificationService是IVGuard提醒子系统的核心服务之一,负责所有通知栏提醒的发布和管理。该服务基于HarmonyOS NotificationKit API构建,提供统一的通知发布接口和完善的错误处理机制。整个服务设计为约50行核心代码,追求简洁高效。
2.1 HarmonyOS NotificationKit API
HarmonyOS的NotificationKit提供了完整的通知管理能力,从通知发布、更新、取消到通知权限管理,覆盖了移动端通知场景的全部需求。IVGuard主要使用以下API:
核心API概览
ypescript import { notificationManager } from '@kit.NotificationKit'
otificationManager.publish(request) — 发布通知
otificationManager.cancel(id) — 取消指定通知
otificationManager.cancelAll() — 取消所有通知
otificationManager.isNotificationEnabled() — 检查通知是否启用
otificationManager.requestEnableNotification() — 请求启用通知
NotificationKit的API设计遵循HarmonyOS统一的异步编程模型,所有可能阻塞的API都返回Promise,支持async/await语法。IVGuard的所有通知操作都采用async/await模式,确保代码的清晰性和可维护性。
NotificationRequest构建
NotificationRequest是通知发布的核心数据结构,定义了通知的所有属性,包括ID、内容、样式、行为等。IVGuard中使用的基本文本通知构建方式如下:
ypescript const request: notificationManager.NotificationRequest = { id: Date.now(), content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: message } } }
各字段含义解析:
- id:通知的唯一标识符。使用Date.now()生成时间戳作为ID,确保每次通知都有唯一标识。这在需要取消或更新特定通知时非常重要。如果使用固定ID,新通知会覆盖旧通知,而使用时间戳ID确保每条通知独立显示。
- notificationContentType:通知内容类型,决定通知的展示样式。这是必填字段,系统根据此字段选择对应的渲染模板。
- normal:基本文本通知的内容定义,包含 itle(标题,显示在通知顶部)和 ext(正文,显示在标题下方)。
NotificationRequest还支持更多可选配置:
ypescript const request: notificationManager.NotificationRequest = { id: Date.now(), slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, isOngoing: false, isUnremovable: false, deliveryTime: new Date().getTime(), tapDismissed: true, autoDeletedTime: 0, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: message } } }
其中:
- slotType:通知槽类型,影响通知的默认行为(声音、振动、横幅等)
- isOngoing:是否为持续性通知(如音乐播放器),此类通知不可滑动删除
- isUnremovable:是否不可移除,仅在特定系统通知中使用
- deliveryTime:通知发送时间,用于通知排序
- tapDismissed:点击通知后是否自动消失
- autoDeletedTime:自动删除时间(秒),0表示不自动删除
ContentType枚举详解
HarmonyOS NotificationKit支持四种通知内容类型,每种类型适用于不同的信息展示需求:
| ContentType | 枚举值 | 用途 | IVGuard使用场景 |
|---|---|---|---|
| BASIC_TEXT | NOTIFICATION_CONTENT_BASIC_TEXT | 标题+简短正文 | 低液位预警、流速异常提醒 |
| LONG_TEXT | NOTIFICATION_CONTENT_LONG_TEXT | 标题+长正文 | 输液完成详情、历史记录摘要 |
| MULTI_LINE | NOTIFICATION_CONTENT_MULTI_LINE | 标题+多行文本 | 多参数监控汇报、综合状态通知 |
| PICTURE | NOTIFICATION_CONTENT_PICTURE | 标题+图片 | 液位趋势图通知(未来扩展) |
在IVGuard的当前版本中,主要使用BASIC_TEXT类型满足基本预警需求。未来版本计划引入LONG_TEXT类型展示更详细的输液状态信息,以及PICTURE类型在通知中直接展示液位趋势图表,让护士无需打开应用即可快速了解患者输液变化趋势。
LONG_TEXT通知的构建示例(未来版本):
ypescript const request: notificationManager.NotificationRequest = { id: Date.now(), content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_LONG_TEXT, normal: { title: '输液完成报告', text: '李明(12床)的输液已完成', longText: '患者:李明 | 床号:12 | 开始时间:14:30 | 完成时间:16:45 | 总时长:2小时15分 | 输液量:500ml | 平均流速:3.7ml/min | 异常次数:0' } } }
对于Warning和Danger级别的通知,可以通过设置NotificationRequest的slotType参数来控制通知的展示优先级和提醒方式:
ypescript const request: notificationManager.NotificationRequest = { id: Date.now(), slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: '⚠️ 输液预警', text: message } } }
SlotType的选择影响通知的默认行为:SOCIAL_COMMUNICATION类型的通知默认会弹出横幅并发出提示音,适合Warning级别;而SERVICE_INFORMATION类型的通知则更为安静,适合Info级别。
SlotType完整枚举及推荐用法:
| SlotType | 说明 | IVGuard推荐级别 |
|---|---|---|
| UNKNOWN_TYPE | 未知类型 | 不使用 |
| SOCIAL_COMMUNICATION | 社交通信 | Warning、Danger |
| SERVICE_INFORMATION | 服务信息 | Info |
| CONTENT_INFORMATION | 内容信息 | 不使用 |
| OTHER_TYPES | 其他类型 | 不使用 |
2.2 三种预警通知
IVGuard定义了三种核心预警通知类型,分别对应输液监控场景中最关键的三类事件。每种通知类型都有专门的发送方法和差异化的内容策略。

低液位预警 — sendLevelAlert
低液位预警是最频繁触发的通知类型,当输液液位降至用户设置的预警阈值以下时自动触发。该通知需要包含患者身份标识和当前液位信息,帮助护士快速定位需要处理的患者。
ypescript static async sendLevelAlert(patientName: string, level: number): Promise<void> { const title = '输液预警' const message = ${patientName}的输液液位已降至%,请及时处理 const request: notificationManager.NotificationRequest = { id: Date.now(), slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: message } } } await notificationManager.publish(request) }
设计要点:
- 患者姓名嵌入:通知消息中直接包含患者姓名,护士无需打开应用即可知道是哪位患者需要关注。这在同时监控多位患者时尤为重要。在实际住院场景中,护士可能同时负责6-10位患者,快速定位是哪位患者需要处理是效率的关键。
- 液位百分比:精确到个位数的液位百分比让护士能够判断紧急程度。液位19%和液位5%虽然都触发了预警,但处理优先级明显不同。19%的液位意味着还有大约30-40分钟的缓冲时间,而5%的液位可能只有5-10分钟。
- 行动指引:消息末尾的"请及时处理"提供了明确的行动建议,降低护士的认知负担。没有行动指引的通知只是"信息",而带有行动指引的通知才是"提醒"。
- SlotType选择:使用SOCIAL_COMMUNICATION确保通知以横幅形式弹出,配合系统提示音引起注意。
参数设计考量:
- patientName使用字符串而非患者ID,因为通知是给人看的,姓名比ID更直观
- level使用整数百分比,避免浮点数显示(如"19.3%")增加阅读负担
- 方法返回Promise而非boolean,因为调用方不需要知道通知是否成功(错误在内部处理)
流速异常预警 — sendFlowAnomalyAlert
流速异常预警检测输液速度的偏离情况,包括流速过快(可能导致心脏负担)和流速过慢(可能导致回血或堵管)。这是IVGuard中最需要即时响应的通知类型之一。
ypescript static async sendFlowAnomalyAlert( patientName: string, currentRate: number, expectedRate: number ): Promise<void> { const deviation = Math.abs(currentRate - expectedRate) / expectedRate * 100 const direction = currentRate > expectedRate ? '过快' : '过慢' const title = '流速异常预警' const message = ${patientName}的输液流速,当前滴/分,预期滴/分,偏差% const request: notificationManager.NotificationRequest = { id: Date.now(), slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: message } } } await notificationManager.publish(request) }
设计要点:
- 方向指示:明确告知流速是"过快"还是"过慢",帮助护士快速判断风险类型。流速过快和过慢的临床风险完全不同,处理方式也不同。流速过快可能导致循环负荷过重,特别是对心功能不全的患者;流速过慢可能导致输液管路堵塞或回血。
- 数值对比:同时展示当前流速和预期流速,让护士无需查阅记录即可判断偏差程度。这种"对比呈现"的信息设计比单独展示当前值更高效。
- 偏差百分比:量化的偏差百分比提供了直观的异常严重程度指标,便于护士分诊处理。偏差10%和偏差50%的处理优先级截然不同。
- 高优先级:流速异常可能直接威胁患者安全,因此该通知使用最高的优先级设置。
流速异常的判断逻辑需要设置合理的触发阈值。当前版本采用固定偏差百分比(如30%)作为触发条件,未来版本计划引入自适应阈值算法,根据患者历史数据动态调整异常判断标准,减少误报率。
流速异常的临床背景知识:
| 流速异常类型 | 可能原因 | 临床风险 | 处理方式 |
|---|---|---|---|
| 流速过快 | 调节器松动、体位改变、输液瓶高度过高 | 循环负荷过重、心衰 | 降低输液瓶高度、重新调节流速 |
| 流速过慢 | 管路扭曲、针头移位、静脉痉挛 | 堵管、回血 | 检查管路、重新穿刺 |
输液完成通知 — sendCompletionNotice
输液完成通知是输液监控流程的终点事件,标志着一个输液周期的结束。该通知需要明确告知输液已完成,并引导用户进行下一步操作(如通知护士拔针)。
ypescript static async sendCompletionNotice(patientName: string): Promise<void> { const title = '输液完成' const message = ${patientName}的输液已完成,请通知护士处理 const request: notificationManager.NotificationRequest = { id: Date.now(), slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: message } } } await notificationManager.publish(request) }
设计要点:
- 行动指引明确:"请通知护士处理"提供了明确的下一步操作指引,避免患者或家属在输液结束后不知所措。特别是在门诊输液室,患者可能不知道输液结束后需要呼叫护士拔针。
- 不使用"警告"措辞:输液完成是正常事件,不应使用"警告"或"异常"等措辞引起不必要的紧张。标题使用"输液完成"而非"输液结束",因为"完成"暗示正常结束,而"结束"可能暗示意外终止。
- 通知持久化:输液完成通知应当保持直到用户主动清除,确保不会因为自动消失而被忽略。可以通过设置autoDeletedTime为0实现永久保留。
- 不需要液位参数:输液完成时液位必然为0%,无需在消息中重复展示。
输液完成通知与低液位预警的关系:在大多数情况下,输液完成通知会在低液位预警之后触发。也就是说,用户会先收到"液位降至XX%"的预警,然后在一段时间后收到"输液完成"的通知。这种先后顺序帮助用户逐步感知输液进度,避免突然从无预警到完成的突兀感。
NotificationService完整实现
将上述三种通知方法整合到NotificationService类中,并添加权限管理和错误处理,形成完整的服务实现:
` ypescript
import { notificationManager } from ‘@kit.NotificationKit’
import { hilog } from ‘@kit.PerformanceAnalysisKit’
import { BusinessError } from ‘@kit.BasicServicesKit’
const TAG = ‘IVGuard-NotificationService’
const DOMAIN = 0x0001
export class NotificationService {
static async sendLevelAlert(patientName: string, level: number): Promise {
const title = ‘输液预警’
const message = ${patientName}的输液液位已降至%,请及时处理
await NotificationService.publishNotification(title, message)
}
static async sendFlowAnomalyAlert(
patientName: string,
currentRate: number,
expectedRate: number
): Promise {
const direction = currentRate > expectedRate ? ‘过快’ : ‘过慢’
const title = ‘流速异常预警’
const message = ${patientName}的输液流速,当前滴/分,预期滴/分
await NotificationService.publishNotification(title, message)
}
static async sendCompletionNotice(patientName: string): Promise {
const title = ‘输液完成’
const message = ${patientName}的输液已完成,请通知护士处理
await NotificationService.publishNotification(title, message)
}
private static async publishNotification(
title: string,
message: string
): Promise {
try {
const request: notificationManager.NotificationRequest = {
id: Date.now(),
slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
content: {
notificationContentType:
notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: { title: title, text: message }
}
}
await notificationManager.publish(request)
hilog.info(DOMAIN, TAG, Notification published: )
} catch (err) {
const e = err as BusinessError
hilog.error(DOMAIN, TAG, Notification failed: - )
}
}
static async requestNotificationPermission(): Promise {
try {
const enabled = await notificationManager.isNotificationEnabled()
if (!enabled) {
await notificationManager.requestEnableNotification()
}
return true
} catch (err) {
const e = err as BusinessError
hilog.error(DOMAIN, TAG, Permission request failed: )
return false
}
}
}
`
2.3 通知权限申请
HarmonyOS对通知权限采用"运行时授权"模型,应用需要在使用通知功能前确认权限状态并在必要时请求授权。IVGuard的通知权限管理流程设计如下:
requestNotificationPermission流程
应用启动 │ ▼ 调用 isNotificationEnabled() │ ├── 返回 true ──→ 权限已授予,正常使用通知功能 │ └── 返回 false ──→ 调用 requestEnableNotification() │ ├── 用户同意 ──→ 权限已授予 │ └── 用户拒绝 ──→ 返回 false,降级处理
实现代码:
ypescript static async requestNotificationPermission(): Promise<boolean> { try { const enabled = await notificationManager.isNotificationEnabled() if (enabled) { hilog.info(DOMAIN, TAG, 'Notification already enabled') return true } hilog.info(DOMAIN, TAG, 'Requesting notification permission...') await notificationManager.requestEnableNotification() const recheck = await notificationManager.isNotificationEnabled() hilog.info(DOMAIN, TAG, Permission result: ) return recheck } catch (err) { const e = err as BusinessError hilog.error(DOMAIN, TAG, Permission error: - ) return false } }
流程详解:
- 检查当前状态:首先调用isNotificationEnabled()检查通知是否已经启用。这是必须的第一步,因为如果通知已经启用,就不需要重复请求,避免给用户弹出不必要的对话框。
- 请求授权:如果通知未启用,调用
equestEnableNotification()。这个API会弹出系统对话框,让用户选择是否允许应用发送通知。对话框的文案由系统提供,应用无法自定义。 - 二次确认:请求后再次检查isNotificationEnabled(),因为用户可能在对话框中选择了拒绝。二次确认确保返回值准确反映最终的权限状态。
- 错误处理:整个流程包裹在try-catch中,任何异常都被捕获并记录日志,不会导致应用崩溃。
权限申请时机
权限申请的时机选择至关重要。过早申请(如应用启动时)会让用户感到困惑——用户还没有了解应用的用途就被要求授权,很可能拒绝。过晚申请则可能导致关键时刻通知无法发出。IVGuard的权限申请策略:
- 首次启动引导:在EntryAbility的onCreate阶段检查通知权限状态,但仅在用户首次使用时弹出授权对话框。首次使用判断通过Preferences中存储的标记实现。
- 监控页进入:每次进入MonitorPage前检查权限状态,如果权限被收回则重新提示。这是最关键的检查点,因为监控页面是触发通知的核心场景。
- 设置页:在设置页面提供"开启通知权限"的入口,引导用户前往系统设置手动开启。当用户拒绝通知授权后,
equestEnableNotification()可能无法再次弹出对话框,此时需要引导用户到系统设置中手动开启。 - 权限变更监听:通过on(‘enabledNotificationChanged’)监听通知权限变更,当用户在系统设置中开启通知时自动更新应用状态。
ypescript notificationManager.on('enabledNotificationChanged', (data: notificationManager.EnableNotificationStatus) => { if (data.enable) { hilog.info(DOMAIN, TAG, 'Notification permission granted by user') } else { hilog.warn(DOMAIN, TAG, 'Notification permission revoked by user') } })
NOTIFICATION_CONTROLLER权限配置
在module.json5中声明通知权限:
json5 { "module": { "name": "entry", "requestPermissions": [ { "name": "ohos.permission.NOTIFICATION_CONTROLLER" } ] } }
注意:NOTIFICATION_CONTROLLER权限在HarmonyOS不同版本中的申请方式可能有所不同。在API 10+中,通知权限更多采用运行时授权模式,而非静态声明模式。开发者需要根据目标API版本选择正确的权限申请方式。
权限声明的注意事项:
- ohos.permission.NOTIFICATION_CONTROLLER是系统核心权限,普通应用可能无法直接通过声明获得
- 在API 12+中,推荐使用
otificationManager.requestEnableNotification()的运行时请求方式 - 对于HarmonyOS NEXT(纯鸿蒙版本),权限模型可能有进一步调整,需要查阅最新文档
2.4 错误处理
通知服务的错误处理是保障系统稳定性的关键环节。在医疗场景中,通知发送失败可能导致严重后果,因此错误处理策略必须全面且可靠。
BusinessError捕获
HarmonyOS的NotificationKit API在调用失败时会抛出BusinessError,其中包含错误码和错误消息。IVGuard对所有通知API调用都进行了try-catch包装:
ypescript try { await notificationManager.publish(request) } catch (err) { const e = err as BusinessError hilog.error(DOMAIN, TAG, Notification failed: code=, msg=) }
BusinessError的结构:
ypescript interface BusinessError extends Error { code: number // 错误码 message: string // 错误消息 }
常见的BusinessError错误码及其含义:
| 错误码 | 含义 | IVGuard处理策略 |
|---|---|---|
| 1600001 | 内部错误 | 记录日志,重试一次 |
| 1600002 | 对象不存在 | 检查request构建,记录日志 |
| 1600003 | 通知ID不存在 | 忽略(cancel时可能已自动消失) |
| 1600004 | 通知使能未开启 | 引导用户开启通知权限 |
| 1600005 | 通知策略阻止发布 | 降级到应用内提醒 |
| 1600009 | 通知类型不匹配 | 降级到BASIC_TEXT类型重试 |
针对不同错误码的差异化处理:
ypescript private static handleNotificationError(error: BusinessError): void { switch (error.code) { case 1600001: hilog.error(DOMAIN, TAG, 'Internal error, will retry') break case 1600004: hilog.warn(DOMAIN, TAG, 'Notification not enabled, requesting permission') NotificationService.requestNotificationPermission() break case 1600005: hilog.warn(DOMAIN, TAG, 'Notification blocked by policy, showing in-app alert') break default: hilog.error(DOMAIN, TAG, Unknown error: ) break } }
hilog记录
IVGuard使用HarmonyOS的hilog工具进行日志记录,配置了统一的日志标签和域标识:
` ypescript
const TAG = ‘IVGuard-NotificationService’
const DOMAIN = 0x0001
hilog.info(DOMAIN, TAG, ‘Notification published successfully’)
hilog.warn(DOMAIN, TAG, ‘Notification permission not granted’)
hilog.error(DOMAIN, TAG, Publish failed: )
`
日志级别的使用规范:
- debug:开发阶段的详细调试信息,发布版本中不输出。如通知请求的完整内容、权限检查的详细流程。
- info:正常操作记录,如通知成功发送、权限检查通过。这些日志用于生产环境的运行状态监控。
- warn:非关键性问题,如权限未授予、通知被策略阻止。这些问题不影响应用运行,但需要关注。
- error:操作失败,如通知发送异常、权限申请被拒绝。这些是需要在发布后排查的问题。
日志的隐私保护:
- 患者姓名在日志中脱敏处理(如"李**“而非"李明”)
- 不记录完整的通知内容到日志中
- 日志级别在发布版本中设置为info以上,过滤debug输出
通知发送失败时的降级策略
当通知发送失败时,IVGuard实施多层降级策略确保用户仍能收到提醒:
- 第一次重试:使用原始NotificationRequest重试发布,可能是临时性系统错误(如通知服务短暂忙碌)。
- 降级重试:将ContentType降级为BASIC_TEXT,移除所有可选参数(如slotType、isOngoing等),使用最简配置重试。这可以排除因参数配置不当导致的发布失败。
- 应用内提醒:如果通知栏发布完全失败,在应用内显示一个AlertDialog,确保用户在应用内能够看到提醒信息。这是最后的保障,仅在通知栏完全不可用时使用。
- 语音补偿:如果用户开启了语音提醒,语音播报作为通知栏的补充通道,即使通知栏失败,语音仍然可以触达用户。
ypescript private static async publishWithFallback( title: string, message: string ): Promise<void> { try { await NotificationService.publishNotification(title, message) return } catch (err) { hilog.warn(DOMAIN, TAG, 'Primary notification failed, trying fallback...') } try { await NotificationService.publishBasicNotification(title, message) return } catch (err) { hilog.warn(DOMAIN, TAG, 'Fallback notification also failed') } NotificationService.showInAppAlert(title, message) }
降级策略的触发条件与处理方式对照:
| 降级层级 | 触发条件 | 处理方式 | 用户感知 |
|---|---|---|---|
| 原始通知 | 正常流程 | 标准通知发布 | 通知栏弹出 |
| 简化通知 | 原始通知失败 | 最简参数重试 | 通知栏弹出(可能样式不同) |
| 应用内弹窗 | 通知栏完全不可用 | AlertDialog | 应用内弹窗覆盖 |
| 语音补偿 | 通知栏+弹窗均失败 | 语音播报 | 听觉提醒 |
这种分层降级策略确保了即使在最不利的情况下(系统通知服务异常、权限被收回等),用户仍然能够通过某种渠道接收到关键的输液预警信息。
3. SpeechService设计
SpeechService负责IVGuard中的语音提醒功能,是三通道提醒系统中的重要组成部分。语音提醒能够在用户未主动查看手机的情况下通过听觉通道传递预警信息,特别适合患者端近距离场景。整个服务设计为约51行核心代码,包含开关控制和三种提醒类型的播放接口。
3.1 HarmonyOS MediaKit AVPlayer
HarmonyOS的MediaKit提供了AVPlayer作为音频和视频播放的核心组件。AVPlayer采用状态机架构,播放器的每个操作都对应特定的状态转换,开发者需要在正确的状态下执行正确的操作。
AVPlayer创建与基本使用
` ypescript
import { media } from ‘@kit.MediaKit’
const avPlayer = await media.createAVPlayer()
`
createAVPlayer()是一个异步工厂方法,返回一个AVPlayer实例。创建过程包括分配系统资源、初始化播放引擎等步骤。创建成功后,播放器处于idle状态,等待数据源设置。
状态机模型
AVPlayer的状态机是其设计的核心概念,理解状态转换是正确使用AVPlayer的前提:
┌──────┐ setFdSrc/ ┌─────────────┐ prepare() ┌──────────┐ │ idle │──urlSrc──────→│ initialized │────────────→│ prepared │ └──────┘ └─────────────┘ └──────────┘ │ ▲ play() │stop() │ │ ▼ │ ┌───────────┐ reset() ┌───────┐ prepare() ┌──────────┐ │ completed │──────────→│ idle │────────────→│ prepared │ └───────────┘ └───────┘ └──────────┘ │ play() │ ▼ ┌───────────┐ on('endOfStream') ┌──────────┐ │ completed │←────────────────────│ playing │ └───────────┘ └──────────┘
关键状态说明:
- idle:初始状态或重置后的状态。在此状态下只能设置数据源(fdSrc或urlSrc),其他操作会导致错误。idle状态是播放器生命周期的起点和终点(通过reset回到idle)。
- initialized:数据源已设置,等待prepare。此状态下可以设置播放参数(如音量、循环模式)。从idle到initialized的转换由设置数据源自动触发。
- prepared:准备完成,可以开始播放。prepare()调用会解析数据源、分配解码器资源、预缓冲数据。此状态下调用play()开始播放。
- playing:正在播放中。on(‘endOfStream’)回调触发后进入completed状态。在playing状态下可以调用pause()暂停、seek()跳转。
- completed:播放完成。调用reset()回到idle状态,可以重新设置数据源;调用play()从头重新播放。
状态转换的严格约束:
| 当前状态 | 允许的操作 | 非法操作(会抛出异常) |
|---|---|---|
| idle | setFdSrc, setUrlSrc, release | play, pause, seek, stop |
| initialized | prepare, release | play, pause, seek |
| prepared | play, release | setFdSrc, setUrlSrc |
| playing | pause, seek, stop, release | prepare, setFdSrc |
| completed | reset, play, release | pause, seek |
on(‘stateChange’)回调处理
IVGuard通过状态变化回调来管理播放器的生命周期,实现自动化的播放流程控制:
ypescript avPlayer.on('stateChange', (state: string) => { switch (state) { case 'idle': break case 'initialized': avPlayer.prepare() break case 'prepared': avPlayer.play() break case 'playing': hilog.info(DOMAIN, TAG, 'Alert audio is playing') break case 'completed': avPlayer.release() break default: break } })
这种状态驱动的设计确保了播放操作在正确的时机执行,避免了在错误状态下调用API导致的异常。整个播放流程通过回调链自动完成:设置数据源→initialized→prepare→prepared→play→playing→endOfStream→completed→release。
回调链的优势:
- 无需手动管理状态:开发者只需设置回调函数,播放流程自动推进
- 避免时序错误:每个操作都在正确的状态下执行,不会出现"在idle状态调用play"这类错误
- 代码简洁:相比手动检查状态再执行操作,回调链代码更简洁直观
release()释放资源
音频播放器是系统资源密集型对象,使用完毕后必须及时释放:
ypescript avPlayer.release().then(() => { hilog.info(DOMAIN, TAG, 'AVPlayer released') }).catch((err: BusinessError) => { hilog.error(DOMAIN, TAG, Release failed: ) })
在completed状态回调中调用release()是最安全的释放时机,因为此时播放器已经完成了所有工作,不存在正在进行的播放操作。如果在playing状态下强制释放,可能导致音频突然中断或系统资源泄漏。
资源释放的重要性:
- 内存:AVPlayer实例占用数MB的内存,不及时释放会导致内存泄漏
- 音频焦点:播放器持有音频焦点时不释放,其他应用无法正常播放音频
- 文件描述符:fdSrc使用的文件描述符在release时关闭,不释放会导致fd泄漏
- 解码器:系统解码器资源有限,不及时释放可能影响其他应用的音频播放
3.2 音频文件播放方案
IVGuard的语音提醒当前采用音频文件播放方案,预置不同的音频文件对应不同类型的提醒。
当前Mock状态
当前版本的SpeechService处于Mock状态,没有实际的音频文件。当调用语音播放方法时,仅记录日志而不执行实际播放:
ypescript static async playLowLevelAlert(): Promise<void> { if (!SpeechService.enabled) { hilog.info(DOMAIN, TAG, 'Speech alert disabled, skipping') return } hilog.info(DOMAIN, TAG, 'Mock: would play low level alert audio') }
Mock设计的原因:
- 音频素材需要专业录制或合成,当前开发阶段优先完成架构和逻辑
- Mock模式允许在不依赖音频资源的情况下测试完整的提醒流程
- 后续替换为真实音频时,仅需修改SpeechService内部实现,不影响调用方
- Mock方法返回Promise与真实实现的签名一致,确保接口兼容性
Mock模式的测试价值:
- 验证提醒流程的完整性:从触发条件判断到调用SpeechService方法
- 验证开关控制逻辑:enabled为false时跳过播放
- 验证多通道并行触发:通知栏+语音+振动的时序关系
- 验证防重复触发:alertTriggered标志的设置和重置
真实方案:rawfile音频文件
生产环境的音频文件播放方案如下:
文件结构:
resources/ rawfile/ alert_low.mp3 — 低液位预警音 alert_complete.mp3 — 输液完成提示音 alert_anomaly.mp3 — 流速异常预警音
播放流程:
` ypescript
import { resourceManager } from ‘@kit.LocalizationKit’
import { media } from ‘@kit.MediaKit’
import { fileIo } from ‘@kit.CoreFileKit’
static async playAlertAudio(fileName: string): Promise {
if (!SpeechService.enabled) {
return
}
const context = getContext(this)
const resMgr = context.resourceManager
const fileFd = resMgr.getRawFd(fileName)
const avPlayer = await media.createAVPlayer()
avPlayer.on(‘stateChange’, (state: string) => {
switch (state) {
case ‘initialized’:
avPlayer.prepare()
break
case ‘prepared’:
avPlayer.play()
break
case ‘completed’:
avPlayer.release()
break
default:
break
}
})
avPlayer.fdSrc = { fd: fileFd.fd, offset: fileFd.offset, length: fileFd.length }
}
`
播放流程详解:
- 创建播放器:调用media.createAVPlayer()创建AVPlayer实例,此时播放器处于idle状态
- 注册回调:在设置数据源前注册stateChange回调,确保不遗漏任何状态变化。注册顺序很重要——如果先设置数据源再注册回调,可能在回调注册前就已经发生了状态转换(idle→initialized),导致回调链断裂
- 设置数据源:通过fdSrc属性设置rawfile的文件描述符,这会触发状态从idle转换到initialized。rawfile通过resourceManager.getRawFd()获取文件描述符,包含fd、offset和length三个参数
- 准备播放:initialized状态下回调函数自动调用prepare(),进入prepared状态
- 开始播放:prepared状态下回调函数自动调用play(),进入playing状态
- 播放完成:on(‘endOfStream’)触发,进入completed状态
- 释放资源:completed状态下回调函数自动调用release(),释放播放器资源
fdSrc参数说明:
ypescript avPlayer.fdSrc = { fd: fileFd.fd, // 文件描述符 offset: fileFd.offset, // 数据在文件中的偏移量 length: fileFd.length // 数据长度 }
rawfile在HAP包中是压缩存储的,getRawFd()返回的是解压后的文件描述符信息。offset和length参数指定了音频数据在文件中的位置和大小,确保播放器只读取有效的音频数据。
音频文件格式要求:
- 推荐使用MP3格式,兼容性最好,HarmonyOS的所有版本都支持
- 采样率:44100Hz(CD音质)或22050Hz(语音质量足够)
- 比特率:128kbps(语音内容不需要太高比特率,64kbps也可接受)
- 声道:单声道即可,语音提醒不需要立体声
- 时长:3-5秒为宜,过长会打扰用户,过短则信息传达不充分
- 音量:录制时标准化到-3dB,确保在各种设备上都有足够的音量
- 格式验证:确保音频文件头信息完整,损坏的文件头会导致AVPlayer初始化失败
3.3 TTS语音合成方案
除了预录音频方案外,IVGuard还评估了基于Text-to-Speech(TTS)的语音合成方案。HarmonyOS提供了AISpeechKit中的textToSpeech API,可以实时将文本转换为语音。
@kit.AISpeechKit textToSpeech
` ypescript
import { textToSpeech } from ‘@kit.AISpeechKit’
const ttsEngine = textToSpeech.createEngine(‘zh-CN’)
ttsEngine.speak(‘您的输液即将完成,请留意’, {
pitch: 1.0,
speed: 1.0,
volume: 2
})
`
TTS引擎的初始化和使用流程:
- 创建引擎:调用 extToSpeech.createEngine()创建TTS引擎实例,参数为语言代码(如’zh-CN’表示中文普通话)
- 配置参数:设置语速、音调、音量等参数
- 合成语音:调用speak()方法将文本转换为语音并播放
- 停止和释放:使用完毕后调用shutdown()释放引擎资源
TTS方案的优势
- 动态生成话术:TTS可以根据实时数据动态生成提醒内容。例如,"张三的输液液位已降至15%"这样的消息,其中患者姓名和液位数值都是运行时确定的,预录音频无法实现这种动态性。这意味着同一条语音消息可以包含精确的实时数据,提供更有价值的信息。
- 无需预录音频:不需要录制和维护音频文件,减少了资源管理的工作量。也避免了音频文件格式兼容、存储空间占用等问题。
- 多语言支持:TTS引擎通常支持多种语言,方便未来扩展国际化功能。只需切换语言代码即可生成不同语言的语音,无需为每种语言录制音频。
- 内容灵活:修改提醒话术只需修改文本模板,无需重新录制音频。这在需要频繁调整话术内容的项目初期特别有价值。
- 个性化定制:可以根据用户偏好选择不同的语音风格(男声/女声、语速、音调等),提升用户体验。
TTS方案的劣势
- 依赖AI能力:TTS功能依赖设备端的AI能力或云端服务。在设备不支持AI能力或网络不可用的场景下,TTS无法工作。这在医疗场景中是一个严重的问题——医院某些区域(如地下室、屏蔽室)网络信号弱,云端TTS可能不可用。
- 延迟较高:TTS的文本转语音过程需要计算时间,从调用speak()到实际发声通常有200-500ms的延迟。在紧急预警场景下,这个延迟可能不可接受。对比之下,预录音频的播放延迟通常在50ms以内。
- 语音质量:虽然TTS技术已相当成熟,但在某些特殊词汇(如医学术语、药名)上的发音可能不够自然或准确。例如,"滴/分"的语音合成可能不够清晰,而预录音频可以确保发音准确。
- 资源消耗:TTS引擎的初始化和运行需要占用一定的CPU和内存资源。在低端设备上,这可能影响应用的流畅度。
- 离线可用性:部分TTS实现依赖云端服务,在没有网络的环境中无法使用。虽然HarmonyOS支持离线TTS,但语音质量通常不如云端方案。
推荐话术设计
如果采用TTS方案,推荐的话术模板设计如下:
ypescript const templates = { lowLevel: (name: string, level: number) => ${name}的输液液位已降至%,请注意观察, flowAnomaly: (name: string, direction: string, rate: number) => ${name}的输液流速,当前滴每分钟, completion: (name: string) => ${name}的输液已完成,请通知护士处理 }
TTS话术的设计原则:
- 口语化:话术应当适合口头表达,而非书面语。例如用"降至"而非"低于",用"请注意观察"而非"请关注"。
- 数字处理:TTS对数字的读法可能不符合预期。如"15%“可能读作"一十五百分之”,需要测试并调整。"滴/分"建议写成"滴每分钟"以确保TTS正确读出。
- 节奏感:话术的长度和节奏应当适合语音播报。过长的句子在口语中难以理解,建议控制在15-20字以内。
- 标点符号:逗号和句号的位置影响TTS的停顿节奏。合理的标点使用可以让语音播报更自然。
混合方案建议
综合考虑预录音频和TTS的优缺点,IVGuard建议采用混合方案:
- 核心提醒音:使用预录音频(alert_low.mp3等),确保零延迟和高质量。核心提醒音的内容固定,不需要动态数据,预录音频的语音质量更可靠。
- 详细信息播报:使用TTS动态生成,提供包含实时数据的详细信息。详细信息在核心提醒音之后播放,延迟可接受。
- 降级策略:TTS不可用时回退到预录音频。确保在所有场景下都有语音输出。
混合方案的实现架构:
ypescript static async playLowLevelAlert(patientName: string, level: number): Promise<void> { if (!SpeechService.enabled) { return } await SpeechService.playAlertAudio('alert_low.mp3') if (SpeechService.ttsAvailable) { SpeechService.speakTTS(${patientName}的输液液位已降至%,请注意观察) } }
这种混合方案兼顾了及时性和灵活性,是医疗IoT场景下的最佳实践。
3.4 开关控制
SpeechService的开关控制是用户可控原则的具体体现。用户可以根据自己的使用场景和偏好独立控制语音提醒的启用状态。
开关接口设计
` ypescript
export class SpeechService {
private static enabled: boolean = true
static setEnabled(val: boolean): void {
SpeechService.enabled = val
hilog.info(DOMAIN, TAG, Speech alert )
}
static isEnabled(): boolean {
return SpeechService.enabled
}
}
`
开关接口的设计考量:
- static方法:SpeechService作为工具类使用,不需要实例化。所有方法都是static的,通过类名直接调用。
- private static字段:enabled状态存储在静态字段中,所有调用共享同一状态。这确保了开关控制的全局一致性。
- setEnabled + isEnabled:setter/getter模式,符合面向对象设计的封装原则。外部代码不应该直接修改enabled字段。
- 日志记录:每次开关变更都记录日志,便于问题排查和用户行为分析。
与AppSettings联动
SpeechService的开关状态与AppSettings中的voiceAlertEnabled设置保持同步。这种联动确保了用户在设置页面修改的偏好能够立即反映到语音服务的行为中:
ypescript const settings = DataStore.loadSettings() SpeechService.setEnabled(settings.voiceAlertEnabled)
联动机制的工作流程:
- 设置变更:用户在SettingsPage修改语音提醒开关
- 持久化:DataStore.saveSettings()将新设置写入持久存储(Preferences)
- 即时生效:设置变更后立即调用SpeechService.setEnabled()更新服务状态
- 应用恢复:应用重启或从后台恢复时,从DataStore加载设置并同步到SpeechService
联动的代码实现(SettingsPage中):
ypescript Toggle({ type: ToggleType.Switch, isOn: this.voiceAlertEnabled }) .onChange((isOn: boolean) => { this.voiceAlertEnabled = isOn const settings = DataStore.loadSettings() settings.voiceAlertEnabled = isOn DataStore.saveSettings(settings) SpeechService.setEnabled(isOn) })
初始化时的联动(EntryAbility或首页aboutToAppear中):
ypescript aboutToAppear(): void { const settings = DataStore.loadSettings() SpeechService.setEnabled(settings.voiceAlertEnabled) VibrationService.setEnabled(settings.vibrationEnabled) }
播放前的开关检查
每个播放方法在执行前都会检查开关状态:
ypescript static async playLowLevelAlert(): Promise<void> { if (!SpeechService.enabled) { hilog.info(DOMAIN, TAG, 'Speech disabled, skip low level alert') return } await SpeechService.playAlertAudio('alert_low.mp3') }
这种设计确保了即使调用方没有检查开关状态,SpeechService自身也会进行拦截,避免在用户已关闭语音的情况下仍然播放音频。这种"防御性编程"在医疗场景中尤为重要——调用方可能因为逻辑复杂而遗漏开关检查,但SpeechService的内部检查能够兜底保护。
3.5 SpeechService完整实现
` ypescript
import { media } from ‘@kit.MediaKit’
import { hilog } from ‘@kit.PerformanceAnalysisKit’
import { BusinessError } from ‘@kit.BasicServicesKit’
const TAG = ‘IVGuard-SpeechService’
const DOMAIN = 0x0001
export class SpeechService {
private static enabled: boolean = true
static setEnabled(val: boolean): void {
SpeechService.enabled = val
hilog.info(DOMAIN, TAG, Speech alert )
}
static isEnabled(): boolean {
return SpeechService.enabled
}
static async playLowLevelAlert(): Promise {
if (!SpeechService.enabled) {
return
}
hilog.info(DOMAIN, TAG, ‘Mock: play low level alert’)
}
static async playCompletionAlert(): Promise {
if (!SpeechService.enabled) {
return
}
hilog.info(DOMAIN, TAG, ‘Mock: play completion alert’)
}
static async playFlowAnomalyAlert(): Promise {
if (!SpeechService.enabled) {
return
}
hilog.info(DOMAIN, TAG, ‘Mock: play flow anomaly alert’)
}
}
`
完整实现的代码行数统计:约51行(含导入和常量声明)。这个精简的实现包含了所有必要的功能:开关控制、三种提醒类型的播放接口、以及Mock日志输出。替换为真实音频播放时,只需在三个play方法中调用playAlertAudio()即可,不需要修改任何外部接口。
4. 提醒话术设计
提醒话术是用户直接感知到的信息内容,其设计质量直接影响用户体验和信息传达效果。IVGuard的提醒话术设计遵循以下原则:
- 简洁明确:每句话术控制在20字以内,确保用户能够在3秒内理解信息要点
- 行动导向:话术不仅描述状态,还提供行动建议
- 角色适配:不同角色(患者/护士/家属)收到不同内容的话术
- 语气得当:避免过度紧急或过于平淡,匹配事件的实际紧急程度
- 无歧义:话术表述应当唯一确定,避免用户产生不同的理解
4.1 患者端话术
患者端话术面向正在接受输液的患者本人,采用直接、友好、引导性的语气:
| 场景 | 话术 | 语气 | 说明 |
|---|---|---|---|
| 低液位预警 | “您的输液即将完成,请留意” | 温和提醒 | 不使用具体百分比,避免引起焦虑 |
| 输液完成 | “输液已完成,请通知护士” | 明确指引 | 直接告知下一步操作 |
| 流速异常 | “检测到流速异常,请查看” | 谨慎提醒 | 不描述具体异常,避免患者自行判断 |
患者端话术设计的关键考虑:
- 避免恐慌:患者不是医疗专业人员,过于详细的技术信息(如"流速偏差35%")可能引起不必要的焦虑。话术采用概括性描述,侧重引导行动。
- 正面引导:使用"请留意"、“请查看"等温和措辞,而非"警告”、“危险"等可能引起恐慌的词汇。“请留意"暗示"注意观察就好”,而非"立即行动”。
- 操作明确:输液完成的话术直接告知"请通知护士",提供明确的操作指引,避免患者不确定下一步该做什么。在临床中,输液完成后需要护士拔针,患者不能自行处理。
- 第一人称:使用"您的"而非"你的",更正式也更有尊重感。医疗场景中的语言应当比日常对话更为正式和尊重。
- 避免技术术语:不使用"液位"、"滴速"等专业术语,使用"输液即将完成"这样的大众化表达。
4.2 护士端话术
护士端话术面向临床护理人员,采用专业、精确、高效的语气:
| 场景 | 话术 | 语气 | 说明 |
|---|---|---|---|
| 低液位预警 | “XX床号患者输液液位降至XX%,请及时处理” | 专业紧急 | 包含床号和精确液位 |
| 输液完成 | “XX床号患者输液已完成,请前往处理” | 清晰明确 | 需要护士执行拔针等操作 |
| 流速异常 | “XX床号患者输液流速异常,当前XX滴/分” | 技术精确 | 包含具体数值便于判断 |
护士端话术设计的核心原则:
- 信息密度高:护士需要从通知中直接获取足够的信息来做出判断和处理决策,无需打开应用查看详情。在繁忙的临床工作中,每减少一次应用操作都是效率的提升。
- 床号标识:在住院场景中,床号是护士定位患者的最快方式,比姓名更实用。因为护士对所管患者的床号非常熟悉,看到床号就能立即定位患者的位置。
- 精确数值:护士具备专业判断能力,精确的液位百分比和流速数值能够帮助护士评估紧急程度和处理优先级。液位15%和液位3%的处理策略完全不同。
- 专业术语:使用"液位"、“流速”、"滴/分"等专业术语,与护士的日常工作语言一致。这不仅提高了信息传达效率,也增强了系统的专业形象。
- 行动差异:低液位时"请及时处理"暗示需要安排时间前往,输液完成时"请前往处理"暗示需要立即前往。措辞的细微差异传达了不同的紧急程度。
4.3 家属端话术
家属端话术面向患者家属,采用关怀、安心、引导的语气:
| 场景 | 话术 | 语气 | 说明 |
|---|---|---|---|
| 低液位预警 | “您关注的亲人输液状态有变化,请查看” | 温和关怀 | 不使用具体数值,避免焦虑 |
| 输液完成 | “您关注的亲人输液已完成” | 安心告知 | 正面信息,不增加焦虑 |
| 流速异常 | “您关注的亲人输液状态异常,已通知医护人员” | 安心引导 | 强调已通知,减少家属担忧 |
家属端话术设计的核心考虑:
- 降低焦虑:家属不在现场,无法直接处理问题,过于详细的异常信息只会增加焦虑而无助于解决问题。告知"状态有变化"而非"液位很低",减少了信息引起的不安。
- 强调系统保障:话术中强调"已通知医护人员",让家属知道系统已经自动通知了专业人员,不需要家属自己联系。这种"系统保障"的表述既传达了信息,又给予了安心感。
- 引导查看:低液位预警时引导家属打开应用查看详情,而非在通知中展示可能引起误解的简略信息。"请查看"暗示打开应用可以看到更详细的状态。
- 正面表达:输液完成使用"已完成"而非"已结束","完成"暗示一切正常,而"结束"可能暗示异常终止。
- 亲属称谓:使用"您关注的亲人"而非"患者XX",更加温情和人性化。
4.4 话术配置化设计
为了支持未来的话术定制和多语言扩展,IVGuard将话术模板设计为可配置结构:
` ypescript
export class AlertMessageTemplates {
static readonly PATIENT = {
lowLevel: ‘您的输液即将完成,请留意’,
completion: ‘输液已完成,请通知护士’,
flowAnomaly: ‘检测到流速异常,请查看’
}
static readonly NURSE = {
lowLevel: (bedNo: string, level: number) =>
${bedNo}床号患者输液液位降至%,请及时处理,
completion: (bedNo: string) =>
${bedNo}床号患者输液已完成,请前往处理,
flowAnomaly: (bedNo: string, rate: number) =>
${bedNo}床号患者输液流速异常,当前滴/分
}
static readonly FAMILY = {
lowLevel: ‘您关注的亲人输液状态有变化,请查看’,
completion: ‘您关注的亲人输液已完成’,
flowAnomaly: ‘您关注的亲人输液状态异常,已通知医护人员’
}
}
`
配置化设计的特点:
- 静态只读:使用static readonly确保话术模板在运行时不可修改,保证一致性
- 模板函数:护士端话术使用函数模板,支持动态参数嵌入(床号、液位等)
- 静态字符串:患者端和家属端话术使用静态字符串,无需参数化
- 按角色分组:PATIENT/NURSE/FAMILY三个命名空间清晰分组,便于维护
这种配置化设计允许:
- 在不修改代码的情况下调整话术内容(未来支持从配置文件加载)
- 根据用户角色自动选择对应的话术模板
- 未来支持多语言时,只需添加对应语言的模板配置(如PATIENT_EN、NURSE_EN等)
- A/B测试不同话术版本的提醒效果
话术配置化的未来扩展方向:
` ypescript
export class AlertMessageTemplates {
private static templates: Map<string, AlertTemplateSet> = new Map()
static init(): void {
AlertMessageTemplates.templates.set(‘zh-CN’, AlertMessageTemplates.getZhCN())
AlertMessageTemplates.templates.set(‘zh-TW’, AlertMessageTemplates.getZhTW())
AlertMessageTemplates.templates.set(‘en’, AlertMessageTemplates.getEN())
}
static getTemplate(locale: string, role: UserRole): RoleTemplateSet {
const set = AlertMessageTemplates.templates.get(locale)
if (!set) {
return AlertMessageTemplates.templates.get(‘zh-CN’)!.get(role)
}
return set.get(role)
}
}
`
多语言话术模板的示例:
ypescript private static getEN(): AlertTemplateSet { return { patient: { lowLevel: 'Your infusion is almost complete, please note', completion: 'Infusion completed, please notify the nurse', flowAnomaly: 'Abnormal flow rate detected, please check' }, nurse: { lowLevel: (bedNo: string, level: number) => Bed infusion level at %, please attend, completion: (bedNo: string) => Bed infusion completed, please proceed, flowAnomaly: (bedNo: string, rate: number) => Bed abnormal flow rate: drops/min }, family: { lowLevel: 'Your loved one's infusion status has changed, please check', completion: 'Your loved one's infusion is complete', flowAnomaly: 'Abnormal infusion status detected, medical staff notified' } } }
4.5 话术的用户体验测试
话术设计不是一次性的工作,而是需要通过用户测试不断迭代优化的过程。IVGuard计划开展以下话术测试:
可理解性测试
- 测试方法:向20名不同角色的用户展示通知内容,测量理解准确率和理解时间
- 合格标准:理解准确率≥95%,平均理解时间≤3秒
- 测试要点:确认用户能够准确理解话术传达的信息和行动建议
紧急程度感知测试
- 测试方法:让用户对不同级别的话术进行紧急程度评分(1-5分)
- 合格标准:Info≤2分,Warning=3分,Danger≥4分,各级别间有显著差异
- 测试要点:确认用户对紧急程度的感知与设计意图一致
焦虑影响测试
- 测量方法:使用焦虑自评量表(SAS)测量用户在收到不同话术后的焦虑水平变化
- 合格标准:患者端话术导致的焦虑水平变化≤5分(SAS量表)
- 测试要点:确认话术不会引起过度的焦虑反应
5. 提醒触发时机
提醒触发时机是提醒系统设计的核心问题——何时触发提醒、如何避免重复触发、如何处理边界情况,这些细节直接决定了系统的实用性和可靠性。
5.1 MonitorPage中的轮询检查
IVGuard的MonitorPage采用3秒间隔的轮询机制来检查输液状态并触发提醒。这个轮询间隔是在及时性和系统资源消耗之间的平衡选择:
ypescript private checkAlert(): void { const settings = DataStore.loadSettings() if (this.currentLevel <= settings.warningThreshold && !this.alertTriggered) { this.alertTriggered = true NotificationService.sendLevelAlert(this.patientName, this.currentLevel) if (settings.voiceAlertEnabled) { SpeechService.playLowLevelAlert() } if (settings.vibrationEnabled) { VibrationService.vibrate(VibrationMode.LONG) } } }
checkAlert()方法的核心逻辑:
- 加载设置:从DataStore获取用户配置的预警阈值和通道开关
- 阈值比较:判断当前液位是否低于预警阈值
- 防重复检查:检查alertTriggered标志,避免重复触发
- 触发通知:调用NotificationService发送通知栏提醒
- 触发语音:如果语音开关开启,调用SpeechService播放语音
- 触发振动:如果振动开关开启,调用VibrationService执行振动
轮询机制详解
轮询检查的完整生命周期:
- 页面加载:MonitorPage的aboutToAppear生命周期中初始化定时器
- 定时触发:每3秒执行一次状态检查
- 数据更新:从蓝牙设备获取最新液位数据,更新currentLevel
- 阈值比较:比较currentLevel与warningThreshold
- 触发提醒:满足条件时触发三通道提醒
- 页面销毁:aboutToDisappear中清除定时器,避免内存泄漏
` ypescript
@Component
export struct MonitorPage {
private patientName: string = ‘’
private currentLevel: number = 100
private alertTriggered: boolean = false
private pollingTimer: number = -1
aboutToAppear(): void {
this.pollingTimer = setInterval(() => {
this.refreshData()
this.checkAlert()
}, 3000)
}
aboutToDisappear(): void {
if (this.pollingTimer !== -1) {
clearInterval(this.pollingTimer)
this.pollingTimer = -1
}
}
}
`
定时器管理的注意事项:
- 初始化为-1:pollingTimer初始值为-1,表示定时器未创建。这避免了在aboutToDisappear中误清除无效定时器。
- 清除后重置:clearInterval后将pollingTimer重置为-1,防止重复清除。
- 先刷新后检查:refreshData()在checkAlert()之前执行,确保检查基于最新数据。
- 生命周期对齐:定时器创建和销毁分别对齐到aboutToAppear和aboutToDisappear,确保生命周期完整。
3秒间隔的技术考量
3秒轮询间隔的选择基于以下考量:
- 临床响应需求:输液液位的变化速度通常在每分钟1-5%的范围内(取决于流速设置)。3秒内液位变化不超过0.25%,在临床上没有显著意义。即使是最快的输液速度,3秒内的液位变化也不足以影响临床决策。
- 蓝牙传输能力:BLE设备的数据传输间隔通常在7.5ms到4s之间(由连接参数决定),3秒间隔给蓝牙通信留有充足的时间余量。即使蓝牙传输偶尔延迟,也不会影响下一次轮询的执行。
- 电池消耗:过于频繁的轮询会加速设备电池消耗。每3秒一次的蓝牙数据读取和UI刷新在功耗方面是可接受的。持续8小时的输液监控大约消耗5-8%的电池电量。
- 系统资源:每3秒一次的数据刷新和阈值检查对系统资源的占用微乎其微。CPU占用率低于1%,内存占用保持稳定。
- 用户体验:3秒间隔意味着液位显示的更新频率为每分钟20次,视觉上呈现平滑的进度变化,不会出现明显的跳跃。
5.2 阈值比较逻辑
阈值比较是提醒触发的核心判断逻辑。IVGuard采用简单直观的阈值比较策略:
ypescript if (this.currentLevel <= settings.warningThreshold && !this.alertTriggered) { // 触发提醒 }
阈值配置
IVGuard支持用户自定义预警阈值(warningThreshold),默认值为20%。阈值的合理设置直接影响提醒的实用性和用户体验:
- 阈值过高(如50%):提醒触发过早,用户收到提醒后可能还需要等待很长时间才需要处理,导致"狼来了"效应
- 阈值过低(如5%):提醒触发过晚,留给用户的反应时间不足,可能错过最佳处理时机
- 推荐值(15-25%):以普通输液速度2ml/min计算,20%液位对应约30分钟的反应时间,足以让护士安排处理
多级阈值
IVGuard的分级提醒实际上对应了多级阈值设置:
ypescript interface AlertThresholds { infoThreshold: number // 信息级阈值,默认50% warningThreshold: number // 预警级阈值,默认20% dangerThreshold: number // 危险级阈值,默认5% }
每一级阈值触发不同的提醒强度:
- 液位 <= 50%:Info级,静默通知
- 液位 <= 20%:Warning级,通知+语音+振动
- 液位 <= 5%:Danger级,全屏通知+循环语音+持续振动
- 液位 <= 0%:输液完成,完成通知
多级阈值的检查逻辑:
` ypescript
private checkAlert(): void {
const settings = DataStore.loadSettings()
if (this.currentLevel <= 0 && !this.completionTriggered) {
this.completionTriggered = true
this.triggerCompletionAlert()
return
}
if (this.currentLevel <= settings.dangerThreshold && !this.dangerTriggered) {
this.dangerTriggered = true
this.triggerDangerAlert()
return
}
if (this.currentLevel <= settings.warningThreshold && !this.alertTriggered) {
this.alertTriggered = true
this.triggerWarningAlert()
return
}
if (this.currentLevel <= settings.infoThreshold && !this.infoTriggered) {
this.infoTriggered = true
this.triggerInfoAlert()
}
}
`
5.3 防重复触发机制
防重复触发是提醒系统设计中的关键问题。如果不加控制,3秒一次的轮询检查在液位持续低于阈值时会每3秒触发一次提醒,形成"提醒风暴",严重影响用户体验。
IVGuard采用布尔状态标志(alertTriggered)实现防重复触发:
ypescript private alertTriggered: boolean = false
防重复触发的工作机制
时间线: T0: level=25% → 不触发(高于阈值) T1: level=22% → 不触发(高于阈值) T2: level=19% → 触发!alertTriggered = true T3: level=17% → 不触发(alertTriggered已为true) T4: level=15% → 不触发(alertTriggered已为true) ... Tn: 用户手动重置 → alertTriggered = false Tn+1: level=14% → 不触发(仍低于阈值但已重置后的首次?需要讨论)
alertTriggered的局限性
简单的布尔标志方案存在一个重要局限:一旦触发后就永久阻止了后续提醒,即使用户已经处理了当前预警并且液位回升后又再次下降,也不会再次提醒。这在某些场景下可能导致遗漏。
更完善的防重复触发策略:
` ypescript
private lastAlertLevel: number = -1
private alertCooldown: number = 0
private checkAlert(): void {
const settings = DataStore.loadSettings()
const now = Date.now()
if (this.currentLevel <= settings.warningThreshold
&& this.lastAlertLevel !== this.currentLevel
&& now > this.alertCooldown) {
this.lastAlertLevel = this.currentLevel
this.alertCooldown = now + 60000 // 1分钟冷却期
this.triggerAlert()
}
if (this.currentLevel > settings.warningThreshold) {
this.lastAlertLevel = -1 // 液位恢复后重置
}
}
`
这种改进方案的特点:
- 液位变化触发:只有液位发生变化时才触发新提醒,避免同一液位重复提醒
- 冷却期:设置1分钟冷却期,防止液位快速波动时产生过多提醒
- 自动重置:液位回升到阈值以上后自动重置触发状态,允许再次预警
防重复触发的策略对比
| 策略 | 优点 | 缺点 | IVGuard选择 |
|---|---|---|---|
| 布尔标志 | 实现简单,逻辑清晰 | 不可自动恢复 | 当前版本 ✓ |
| 冷却期 | 允许自动恢复,避免频繁提醒 | 参数需调优 | 未来版本 |
| 液位变化触发 | 精准触发,避免重复 | 液位微小波动可能误触发 | 未来版本 |
| 人工确认重置 | 最安全,用户掌控 | 需要用户交互 | 可选补充 |
5.4 输液完成检测
输液完成(currentLevel <= 0)是一个特殊的事件,需要与低液位预警区分处理:
` ypescript
private checkAlert(): void {
const settings = DataStore.loadSettings()
if (this.currentLevel <= 0) {
if (!this.completionTriggered) {
this.completionTriggered = true
NotificationService.sendCompletionNotice(this.patientName)
if (settings.voiceAlertEnabled) {
SpeechService.playCompletionAlert()
}
if (settings.vibrationEnabled) {
VibrationService.vibrate(VibrationMode.DOUBLE)
}
}
return
}
if (this.currentLevel <= settings.warningThreshold && !this.alertTriggered) {
this.alertTriggered = true
NotificationService.sendLevelAlert(this.patientName, this.currentLevel)
if (settings.voiceAlertEnabled) {
SpeechService.playLowLevelAlert()
}
}
}
`
输液完成检测的特殊处理:
- 独立触发标志:completionTriggered与alertTriggered独立管理,确保输液完成通知不受低液位预警状态的影响
- 优先级最高:输液完成检查在低液位预警检查之前执行,确保完成事件不会被遗漏
- 无阈值依赖:输液完成的判断条件是currentLevel <= 0,不依赖任何用户配置的阈值
- 不可逆:输液完成后不需要自动重置,因为一个输液周期已经结束
输液完成的边界情况处理:
- 液位跳变:如果蓝牙数据传输中断后恢复,液位可能从30%直接跳到0%。此时应直接触发输液完成通知,跳过低液位预警。
- 负值处理:传感器偶尔可能返回负值(测量误差),应将负值视为0%处理,触发输液完成通知。
- 多设备同时完成:在护士端同时监控多台设备时,可能同时收到多个输液完成通知。应有聚合机制避免通知栏被淹没。
5.5 后台状态下的提醒
当应用处于后台时,提醒逻辑需要特殊处理:
- 长时任务(Long Task):输液监控属于长期后台任务,需要通过Background Task API申请长时任务权限,确保应用在后台时仍能持续轮询和触发提醒。
- 通知栏可见:后台提醒主要依赖通知栏通道,因为应用内UI不可见。
- 语音播放:后台语音播放需要申请音频后台播放权限,否则系统会暂停音频输出。
` ypescript
import { backgroundTaskManager } from ‘@kit.BackgroundTasksKit’
try {
backgroundTaskManager.requestSuspendDelay(‘IVGuard输液监控’, () => {
hilog.info(DOMAIN, TAG, ‘Suspend delay expired’)
})
} catch (err) {
hilog.error(DOMAIN, TAG, ‘Background task request failed’)
}
`
后台长时任务的申请与维护:
` ypescript
import { backgroundTaskManager } from ‘@kit.BackgroundTasksKit’
import { wantAgent } from ‘@kit.AbilityKit’
async function requestContinuousTask(): Promise {
const wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [{ bundleName: ‘com.ivguard.monitor’, abilityName: ‘EntryAbility’ }],
requestCode: 0,
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
}
const agent = await wantAgent.getWantAgent(wantAgentInfo)
backgroundTaskManager.startBackgroundRunning(
getContext(),
backgroundTaskManager.BackgroundMode.DATA_TRANSFER,
agent
)
}
`
后台状态下的提醒策略调整:
| 通道 | 前台行为 | 后台行为 | 原因 |
|---|---|---|---|
| 通知栏 | 正常发布 | 正常发布 | 通知栏是后台最可靠的通道 |
| 语音 | 正常播放 | 需要后台音频权限 | 系统默认限制后台音频 |
| 振动 | 正常振动 | 正常振动 | 振动不受前后台限制 |
| 应用内提醒 | AlertDialog | 不可用 | 后台时应用UI不可见 |
5.6 流速异常触发逻辑
流速异常的触发逻辑与液位预警有所不同。液位是一个单调递减的值,而流速是波动的,需要在波动中识别真正的异常:
` ypescript
private checkFlowAnomaly(): void {
const settings = DataStore.loadSettings()
const expectedRate = settings.expectedFlowRate
const deviationThreshold = settings.flowDeviationThreshold
if (this.currentRate <= 0) {
return
}
const deviation = Math.abs(this.currentRate - expectedRate) / expectedRate
if (deviation >= deviationThreshold && !this.flowAnomalyTriggered) {
this.flowAnomalyTriggered = true
NotificationService.sendFlowAnomalyAlert(
this.patientName,
this.currentRate,
expectedRate
)
if (settings.voiceAlertEnabled) {
SpeechService.playFlowAnomalyAlert()
}
if (settings.vibrationEnabled) {
VibrationService.vibrate(VibrationMode.SHORT)
}
}
if (deviation < deviationThreshold * 0.8) {
this.flowAnomalyTriggered = false
}
}
`
流速异常触发的特殊考虑:
- 偏差阈值:使用相对偏差(百分比)而非绝对偏差(滴/分),因为不同预期流速下的合理偏差范围不同
- 滞后重置:异常解除条件(偏差低于阈值的80%)比触发条件(偏差超过阈值)更宽松,避免在阈值边界反复触发
- 零流速排除:流速为0时可能表示蓝牙断连而非真实流速数据,不应触发异常提醒
- 持续异常:流速异常可能是暂时的(如患者翻身导致管路短暂扭曲),可以设置持续时间阈值(如连续3次检测异常才触发)
6. 振动提醒
振动提醒是IVGuard三通道提醒系统中的触觉通道,通过设备振动马达产生触觉反馈,在用户无法看到屏幕或听到声音的场景下提供有效的信息传达。
6.1 VIBRATE权限
振动功能需要申请系统权限。在HarmonyOS中,振动权限的申请方式如下:
module.json5权限声明
json5 { "module": { "name": "entry", "requestPermissions": [ { "name": "ohos.permission.VIBRATE", "reason": "", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ] } }
权限声明各字段含义:
- name:权限名称,ohos.permission.VIBRATE是振动权限的标准名称
- reason:权限申请理由,引用字符串资源。向用户展示时使用,应当清晰说明为何需要振动权限
- usedScene.abilities:使用此权限的Ability列表
- usedScene.when:权限使用时机,inuse表示仅在使用期间使用,always表示始终可用
运行时权限检查
` ypescript
import { abilityAccessCtrl } from ‘@kit.AbilityKit’
static async checkVibratePermission(): Promise {
const atManager = abilityAccessCtrl.createAtManager()
try {
const grantStatus = await atManager.checkAccessToken(
getContext().applicationInfo.accessTokenId,
‘ohos.permission.VIBRATE’
)
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
} catch (err) {
hilog.error(DOMAIN, TAG, ‘Vibrate permission check failed’)
return false
}
}
`
振动权限在HarmonyOS中属于normal级别权限,通常在安装时自动授予,不需要用户显式确认。但在某些系统版本或安全策略下,可能需要运行时检查权限状态。
6.2 振动模式设计
IVGuard根据不同的预警级别设计了差异化的振动模式,每种模式的振动参数经过精心设计,确保用户能够通过触觉反馈区分不同的预警类型。
振动模式定义
ypescript enum VibrationMode { SHORT, // 短振动:100ms,用于流速异常 LONG, // 长振动:500ms,用于低液位预警 DOUBLE // 双振动:200ms+200ms间隔+200ms,用于输液完成 }
低液位预警 — 长振动(500ms)
长振动采用持续500ms的振动,产生明确的触觉感知。这种振动模式的特点是持续时间较长,能够引起用户的明确注意,但又不会像持续振动那样造成过度干扰:
` ypescript
import { vibrator } from ‘@kit.SensorServiceKit’
vibrator.vibrate(500)
`
500ms的长振动选择基于以下考虑:
- 可感知性:500ms的振动足够长,即使用户将手机放在口袋中也能清晰感知。研究表明,超过300ms的振动在口袋中感知率超过90%。
- 区分度:与常见的短通知振动(50-100ms)有明显的持续时间差异,用户可以区分。普通应用的推送通知通常使用50ms振动,500ms的振动与之有明显区别。
- 舒适度:不过长(如1000ms以上),不会让用户感到不适。医疗场景中需要保持专业和克制,过度强烈的振动反馈是不恰当的。
- 设备兼容性:500ms的振动在所有支持振动的设备上都能正常执行,不受设备振动马达性能差异的影响。
输液完成 — 双振动(200ms+200ms)
双振动采用"振动200ms-暂停200ms-振动200ms"的模式,形成"嗡-嗡"的双脉冲效果。这种模式模仿了传统医疗设备的报警节奏,用户容易将其与紧急提醒关联:
ypescript static async vibrateDouble(): Promise<void> { try { await vibrator.vibrate(200) setTimeout(async () => { await vibrator.vibrate(200) }, 400) } catch (err) { hilog.error(DOMAIN, TAG, 'Double vibrate failed') } }
双振动的设计考虑:
- 节奏辨识:双脉冲节奏与单次振动有明显区别,用户能够通过触觉区分输液完成和低液位预警。这种"节奏编码"让用户无需查看手机就能判断提醒类型。
- 紧急感:双脉冲模仿传统的"紧急"信号模式,传达出需要立即处理的紧迫感。在医院中,双声警报通常表示需要立即关注的事件。
- 间隔设计:200ms的间隔(加上前一次振动结束后的时间,实际间隔约400ms)足以让用户感受到两次独立的振动,但又不至于间隔过长导致节奏感丧失。
- 总时长:双振动的总时长约800ms(200+200+200+间隔),在提供充分提醒效果的同时不会过度打扰。
流速异常 — 短振动(100ms)
短振动采用100ms的快速振动,产生"嗡"一下的轻触效果。流速异常虽然需要关注,但不一定需要立即行动,因此采用较轻的振动模式:
ypescript vibrator.vibrate(100)
100ms短振动的选择理由:
- 信息级提醒:流速异常属于需要关注但不必立即行动的情况,轻振动传达"请注意"而非"立即行动"
- 不干扰:在输液室等安静环境中,频繁的强烈振动可能影响其他患者,短振动更为礼貌
- 快速:短振动执行时间短,不会阻塞后续的提醒逻辑
- 与系统通知区分:100ms短振动比系统默认的推送通知振动(通常50ms)稍长,用户可以感知到这不是普通通知
振动模式与预警级别的完整对照:
| 预警级别 | 振动模式 | 振动参数 | 触觉感受 | 设计意图 |
|---|---|---|---|---|
| Info | 无振动 | — | — | 静默通知,不打扰 |
| Warning (低液位) | 长振动 | 500ms | 持续"嗡" | 引起注意,建议尽快处理 |
| Warning (流速异常) | 短振动 | 100ms | 轻点 | 温和提醒,关注即可 |
| Danger (输液完成) | 双振动 | 200ms+200ms | “嗡-嗡” | 紧急提醒,需要立即行动 |
| Danger (严重异常) | 持续振动 | 1000ms | 长振 | 极度紧急,不能忽略 |
6.3 VibrationService完整实现
` ypescript
import { vibrator } from ‘@kit.SensorServiceKit’
import { hilog } from ‘@kit.PerformanceAnalysisKit’
import { BusinessError } from ‘@kit.BasicServicesKit’
const TAG = ‘IVGuard-VibrationService’
const DOMAIN = 0x0001
export enum VibrationMode {
SHORT,
LONG,
DOUBLE
}
export class VibrationService {
private static enabled: boolean = true
static setEnabled(val: boolean): void {
VibrationService.enabled = val
}
static isEnabled(): boolean {
return VibrationService.enabled
}
static async vibrate(mode: VibrationMode): Promise {
if (!VibrationService.enabled) {
return
}
try {
switch (mode) {
case VibrationMode.SHORT:
await vibrator.vibrate(100)
break
case VibrationMode.LONG:
await vibrator.vibrate(500)
break
case VibrationMode.DOUBLE:
await vibrator.vibrate(200)
setTimeout(() => {
vibrator.vibrate(200)
}, 400)
break
default:
break
}
} catch (err) {
const e = err as BusinessError
hilog.error(DOMAIN, TAG, Vibrate failed: )
}
}
}
`
6.4 与AppSettings.vibrationEnabled联动
振动开关的控制逻辑与语音开关类似,通过AppSettings.vibrationEnabled进行配置:
ypescript const settings = DataStore.loadSettings() VibrationService.setEnabled(settings.vibrationEnabled)
在设置页面的开关回调中同步更新:
ypescript Toggle({ type: ToggleType.Switch, isOn: this.vibrationEnabled }) .onChange((isOn: boolean) => { this.vibrationEnabled = isOn const settings = DataStore.loadSettings() settings.vibrationEnabled = isOn DataStore.saveSettings(settings) VibrationService.setEnabled(isOn) })
6.5 振动与语音的时序协调
振动和语音同时触发时需要考虑时序协调。理想的情况下,振动和语音应当几乎同时开始,形成多感官协同的提醒效果。然而,语音播放的初始化(创建AVPlayer、设置数据源、prepare)需要一定时间,而振动可以立即执行。
IVGuard的时序策略:
- 振动立即执行,不等待语音准备完成
- 语音播放异步执行,准备好后自动开始
- 两者独立触发,不做同步等待
ypescript // 同时触发,各自独立执行 NotificationService.sendLevelAlert(this.patientName, this.currentLevel) if (settings.vibrationEnabled) { VibrationService.vibrate(VibrationMode.LONG) // 立即振动 } if (settings.voiceAlertEnabled) { SpeechService.playLowLevelAlert() // 异步准备后播放 }
这种设计确保了振动提醒的即时性不受语音播放延迟的影响,同时语音播报作为后续补充信息增强了提醒的效果。
时序图:
T+0ms checkAlert()触发 T+0ms ├── NotificationService.sendLevelAlert() → async T+0ms ├── VibrationService.vibrate(LONG) → 立即执行 T+0ms └── SpeechService.playLowLevelAlert() → async T+5ms 振动马达启动 T+50ms 通知栏API调用完成 T+200ms AVPlayer创建完成 T+300ms AVPlayer设置数据源 T+400ms AVPlayer prepare完成 T+450ms 语音开始播放 T+500ms 振动结束
从时序图可以看出,振动在5ms内就产生了触觉反馈,而语音需要约450ms才能开始播放。这种差异在实际使用中并不影响提醒效果,因为振动已经第一时间提醒了用户,语音作为信息补充在半秒内开始播放。
6.6 高级振动模式探索
HarmonyOS的vibrator API除了基本的时长振动外,还支持更丰富的振动模式。未来版本可以考虑以下高级振动模式:
自定义振动效果
` ypescript
vibrator.vibrate({
type: ‘time’,
duration: 500
})
vibrator.vibrate({
type: ‘preset’,
effectId: ‘haptic.clock_tick’
})
`
预设振动效果
HarmonyOS提供了一系列预设的触觉反馈效果,这些效果经过专业设计,触感更加自然:
| 预设效果ID | 描述 | 适用场景 |
|---|---|---|
| haptic.clock_tick | 时钟滴答 | 轻微提示 |
| haptic.context_click | 上下文点击 | 确认操作 |
| haptic.keyboard_press | 键盘按下 | 输入反馈 |
| haptic.keyboard_release | 键盘释放 | 输入完成 |
| haptic.keyboard_tap | 键盘轻触 | 简单反馈 |
| haptic.long_press | 长按 | 重要提醒 |
| haptic.soft_tick | 轻微触感 | 最轻提示 |
| haptic.confirm | 确认 | 操作确认 |
| haptic.bounce | 弹跳 | 弹性效果 |
| haptic.failure | 失败 | 错误提醒 |
| haptic.success | 成功 | 成功反馈 |
| haptic.warning | 警告 | 预警提醒 |
对于IVGuard,haptic.warning预设效果可能比自定义时长的振动更加自然和专业。未来版本可以评估使用预设效果替代自定义时长的方案。
7. 三角色提醒差异化
IVGuard的使用者包括三种角色:患者、护士和家属。不同角色在输液监控场景中的需求、权限和使用场景各不相同,因此提醒系统需要针对不同角色实施差异化的策略。
7.1 角色分析与需求差异
患者
- 场景:近距离使用,手机通常在身边或床头
- 需求:及时感知输液状态变化,知道何时需要呼叫护士
- 环境:可能在休息或睡觉,需要考虑不打扰其他患者
- 技术能力:一般用户水平,需要简单直观的提醒方式
- 关注重点:自身的输液状态,何时需要行动
患者是输液监控的直接受益者,也是提醒系统最频繁的用户。患者通常在输液过程中处于等待状态,可能阅读、休息或与探视者交谈。提醒系统需要在患者注意力分散时能够有效触达,同时避免过度打扰。
护士
- 场景:同时负责多位患者,工作繁忙,手机可能在口袋或推车上
- 需求:快速定位需要处理的患者,获取足够的专业信息做出判断
- 环境:病房走廊或护士站,可能处于嘈杂环境
- 技术能力:专业用户,能够理解医学术语和数据
- 关注重点:多位患者的整体状态,处理优先级排序
护士是输液监控的专业使用者,需要同时关注多位患者。提醒系统需要帮助护士快速识别哪位患者需要关注,并提供足够的信息让护士判断紧急程度和处理优先级。
家属
- 场景:远程关注,可能不在医院内
- 需求:了解亲人输液状态,安心确认输液正常进行
- 环境:日常环境,可能正在工作或做其他事情
- 技术能力:一般用户水平,可能对医疗术语不熟悉
- 关注重点:亲人的安全和舒适,信息的安心感
家属是输液监控的间接关注者,不在现场但关心患者的输液进展。提醒系统需要在不增加家属焦虑的前提下提供状态更新,并强调系统已经自动通知了医护人员。
7.2 差异化提醒方案
患者:语音+振动+通知栏
患者端采用全通道提醒策略,确保输液状态变化能够通过多种方式触达患者:
` ypescript
function triggerPatientAlert(level: number, alertType: AlertType): void {
const settings = DataStore.loadSettings()
NotificationService.sendLevelAlert(patientName, level)
if (settings.voiceAlertEnabled) {
SpeechService.playAlert(alertType)
}
if (settings.vibrationEnabled) {
const mode = alertType === AlertType.COMPLETION
? VibrationMode.DOUBLE
: VibrationMode.LONG
VibrationService.vibrate(mode)
}
}
`
患者端提醒的特点:
- 三通道全开:默认情况下三个通道全部启用,确保最大触达率
- 话术简洁:使用患者端话术模板,内容简洁不引起焦虑
- 夜间模式:可选的夜间模式自动关闭语音和振动,仅保留通知栏
- 重复提醒:输液完成时支持重复提醒(每5分钟一次,最多3次),确保不会遗漏
患者端夜间模式的实现:
` ypescript
function shouldUseNightMode(): boolean {
const now = new Date()
const hour = now.getHours()
return hour >= 22 || hour < 7
}
function triggerPatientAlert(level: number, alertType: AlertType): void {
const settings = DataStore.loadSettings()
const nightMode = shouldUseNightMode()
NotificationService.sendLevelAlert(patientName, level)
if (settings.voiceAlertEnabled && !nightMode) {
SpeechService.playAlert(alertType)
}
if (settings.vibrationEnabled && !nightMode) {
const mode = alertType === AlertType.COMPLETION
? VibrationMode.DOUBLE
: VibrationMode.LONG
VibrationService.vibrate(mode)
}
}
`
护士:通知栏优先(不打扰其他患者)
护士端以通知栏为主要提醒通道,减少语音和振动的使用,避免在病房中打扰其他患者:
` ypescript
function triggerNurseAlert(
bedNo: string,
level: number,
alertType: AlertType
): void {
if (alertType === AlertType.COMPLETION) {
NotificationService.sendCompletionNotice(bedNo)
} else if (alertType === AlertType.FLOW_ANOMALY) {
NotificationService.sendFlowAnomalyAlert(bedNo, currentRate, expectedRate)
} else {
NotificationService.sendLevelAlert(bedNo, level)
}
if (settings.vibrationEnabled && alertType !== AlertType.INFO) {
VibrationService.vibrate(VibrationMode.SHORT)
}
}
`
护士端提醒的特点:
- 通知栏为主:所有预警都通过通知栏传达,护士可以在方便时查看
- 专业信息:通知内容包含床号、精确液位、流速等专业信息
- 不打扰:默认关闭语音,避免在病房中突然播报影响其他患者
- 振动辅助:仅在Warning和Danger级别使用短振动作为辅助提醒
- 批量通知:当多位患者同时触发预警时,通知不会逐条弹出,而是聚合为一条通知,避免通知栏被淹没
护士端批量通知的实现思路:
` ypescript
class NurseAlertAggregator {
private pendingAlerts: Map<string, AlertMessage> = new Map()
private aggregationTimer: number = -1
addAlert(bedNo: string, alert: AlertMessage): void {
this.pendingAlerts.set(bedNo, alert)
if (this.aggregationTimer === -1) {
this.aggregationTimer = setTimeout(() => {
this.flushAlerts()
}, 5000)
}
}
private flushAlerts(): void {
if (this.pendingAlerts.size === 1) {
const [bedNo, alert] = this.pendingAlerts.entries().next().value
NotificationService.sendSingleAlert(bedNo, alert)
} else {
const count = this.pendingAlerts.size
NotificationService.sendAggregatedAlert(count, this.pendingAlerts)
}
this.pendingAlerts.clear()
this.aggregationTimer = -1
}
}
`
家属:通知栏+振动(远程无语音)
家属端主要使用通知栏和振动提醒,不使用语音,因为家属可能不在患者身边,语音播报没有实际意义:
` ypescript
function triggerFamilyAlert(alertType: AlertType): void {
const settings = DataStore.loadSettings()
NotificationService.sendFamilyNotification(alertType)
if (settings.vibrationEnabled && alertType === AlertType.COMPLETION) {
VibrationService.vibrate(VibrationMode.SHORT)
}
}
`
家属端提醒的特点:
- 通知栏为主:通知栏是家属获取信息的主要渠道
- 轻振动:仅在输液完成时使用轻振动提醒
- 安心话术:使用家属端话术模板,强调"已通知医护人员",减少焦虑
- 远程适用:不需要语音播放,家属可以在任何场景下接收通知
7.3 权限控制差异
不同角色需要申请的权限组合不同,这种差异化权限设计遵循最小权限原则,避免申请不必要的权限:
| 权限 | 患者 | 护士 | 家属 |
|---|---|---|---|
| NOTIFICATION | ✅ 必需 | ✅ 必需 | ✅ 必需 |
| VIBRATE | ✅ 必需 | ⚡ 可选 | ⚡ 可选 |
| 后台音频 | ✅ 必需 | ❌ 不需要 | ❌ 不需要 |
| 后台长任务 | ✅ 必需 | ✅ 必需 | ✅ 必需 |
| BLUETOOTH | ✅ 必需 | ✅ 必需 | ❌ 不需要 |
权限申请的差异化实现:
` ypescript
function requestPermissionsByRole(role: UserRole): Promise {
const requiredPermissions: string[] = []
requiredPermissions.push(‘ohos.permission.NOTIFICATION’)
requiredPermissions.push(‘ohos.permission.KEEP_BACKGROUND_RUNNING’)
if (role === UserRole.PATIENT) {
requiredPermissions.push(‘ohos.permission.VIBRATE’)
requiredPermissions.push(‘ohos.permission.BLUETOOTH_CONNECT’)
} else if (role === UserRole.NURSE) {
requiredPermissions.push(‘ohos.permission.BLUETOOTH_CONNECT’)
}
return requestPermissions(requiredPermissions)
}
`
权限差异化的设计理由:
- BLUETOOTH:患者端和护士端需要直接与BLE设备通信获取液位数据,家属端通过远程推送获取信息,不需要蓝牙权限
- VIBRATE:患者端必须使用振动作为重要提醒通道,护士端和家属端振动为辅助功能
- 后台音频:患者端需要在后台播放语音提醒(如患者休息时应用在前台但屏幕关闭),护士端不使用语音,家属端不需要
7.4 角色切换场景
在实际使用中,同一设备可能在不同时间由不同角色使用(例如,家属探视时使用患者的手机查看状态)。IVGuard支持角色切换,切换后提醒策略自动调整:
` ypescript
function switchRole(newRole: UserRole): void {
const settings = DataStore.loadSettings()
settings.currentRole = newRole
switch (newRole) {
case UserRole.PATIENT:
settings.voiceAlertEnabled = true
settings.vibrationEnabled = true
break
case UserRole.NURSE:
settings.voiceAlertEnabled = false
settings.vibrationEnabled = false
break
case UserRole.FAMILY:
settings.voiceAlertEnabled = false
settings.vibrationEnabled = true
break
}
DataStore.saveSettings(settings)
SpeechService.setEnabled(settings.voiceAlertEnabled)
VibrationService.setEnabled(settings.vibrationEnabled)
}
`
角色切换时的处理逻辑:
- 自动调整语音和振动开关状态
- 权限不足时提示用户授权
- 通知栏提醒策略实时更新
- 已触发的提醒不受影响,新触发提醒使用新策略
- 话术模板自动切换到对应角色的版本
角色切换的用户体验考虑:
- 切换时应展示简要的"策略变更说明",告知用户新角色下的提醒行为
- 提供"自定义"选项,允许用户在角色默认配置基础上微调
- 记录角色切换日志,便于用户行为分析和问题排查
7.5 角色差异化提醒的完整对照
| 维度 | 患者 | 护士 | 家属 |
|---|---|---|---|
| 通知栏 | ✅ 必需,含患者姓名+液位 | ✅ 必需,含床号+精确数值 | ✅ 必需,安心话术 |
| 语音 | ✅ 全部级别 | ❌ 默认关闭 | ❌ 不使用 |
| 振动 | ✅ 长/双振动 | ⚡ 仅Warning+短振动 | ⚡ 仅完成时短振动 |
| 话术风格 | 简洁温和 | 专业精确 | 关怀安心 |
| 信息粒度 | 概括(“即将完成”) | 精确(“液位15%”) | 概括+保障(“已通知医护”) |
| 夜间模式 | 支持(关闭语音+振动) | 不适用 | 不适用 |
| 批量通知 | 不适用 | 支持(多患者聚合) | 不适用 |
| 重复提醒 | 支持(输液完成) | 不支持 | 不支持 |
| 蓝牙连接 | 直接连接 | 直接连接 | 远程推送 |
| 后台需求 | 长任务+音频 | 长任务 | 长任务 |
8. 未来:跨设备推送
IVGuard当前版本的提醒系统局限于单设备通知——通知只在运行应用的设备上显示。然而,在真实的医疗场景中,护士可能不在患者身边,家属可能不在医院内,单设备通知的触达能力远远不够。未来版本的IVGuard将实现跨设备推送,确保预警信息能够在任何需要的设备上即时送达。
8.1 护士端远程通知 — WebSocket实时通信
护士站通常配备固定的工作站电脑或平板设备,护士在巡视时随身携带移动终端。通过WebSocket实现护士端的远程通知推送,能够在患者端触发预警时即时通知护士的所有工作设备。
架构设计
患者端设备 服务端 护士端设备 │ │ │ │ BLE检测液位低于阈值 │ │ │──────────────────→ │ │ │ │ WebSocket推送预警消息 │ │ │───────────────────────→│ │ 本地通知+语音+振动 │ │ 护士端通知弹窗 │ │ │ + 声音提示 │ │ │ + 任务队列更新
WebSocket服务端设计
` ypescript
interface AlertMessage {
type: ‘LEVEL_ALERT’ | ‘FLOW_ANOMALY’ | ‘COMPLETION’
patientId: string
bedNo: string
level?: number
currentRate?: number
expectedRate?: number
timestamp: number
}
class AlertWebSocketServer {
private connections: Map<string, WebSocket> = new Map()
registerNurse(nurseId: string, ws: WebSocket): void {
this.connections.set(nurseId, ws)
ws.on(‘close’, () => {
this.connections.delete(nurseId)
})
}
broadcastAlert(message: AlertMessage): void {
const payload = JSON.stringify(message)
this.connections.forEach((ws, nurseId) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(payload)
}
})
}
sendToNurse(nurseId: string, message: AlertMessage): boolean {
const ws = this.connections.get(nurseId)
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message))
return true
}
return false
}
}
`
护士端WebSocket客户端
` ypescript
import { webSocket } from ‘@kit.NetworkKit’
class NurseAlertClient {
private ws: webSocket.WebSocket | null = null
private reconnectAttempts: number = 0
private maxReconnectAttempts: number = 5
async connect(serverUrl: string): Promise {
this.ws = webSocket.createWebSocket()
this.ws.on(‘open’, () => {
hilog.info(DOMAIN, TAG, ‘WebSocket connected to alert server’)
this.reconnectAttempts = 0
})
this.ws.on(‘message’, (err: Error, data: string) => {
if (err) {
hilog.error(DOMAIN, TAG, WebSocket message error: )
return
}
const alert = JSON.parse(data) as AlertMessage
this.handleAlert(alert)
})
this.ws.on(‘close’, () => {
hilog.info(DOMAIN, TAG, ‘WebSocket disconnected, reconnecting…’)
this.scheduleReconnect(serverUrl)
})
this.ws.on(‘error’, (err: Error) => {
hilog.error(DOMAIN, TAG, WebSocket error: )
this.scheduleReconnect(serverUrl)
})
await this.ws.connect(serverUrl)
}
private scheduleReconnect(serverUrl: string): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
hilog.error(DOMAIN, TAG, ‘Max reconnect attempts reached’)
return
}
this.reconnectAttempts++
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000)
setTimeout(() => this.connect(serverUrl), delay)
}
private handleAlert(alert: AlertMessage): void {
switch (alert.type) {
case ‘LEVEL_ALERT’:
NotificationService.sendLevelAlert(alert.bedNo, alert.level!)
break
case ‘FLOW_ANOMALY’:
NotificationService.sendFlowAnomalyAlert(
alert.bedNo,
alert.currentRate!,
alert.expectedRate!
)
break
case ‘COMPLETION’:
NotificationService.sendCompletionNotice(alert.bedNo)
break
}
}
}
`
WebSocket方案的优势:
- 实时性:毫秒级延迟,适合紧急预警场景。WebSocket连接建立后,消息传递延迟通常在10-50ms之间。
- 双向通信:护士端可以向服务端确认接收,实现闭环管理。服务端可以追踪哪些护士已经看到预警,哪些还没有。
- 连接状态感知:断线自动重连,确保连接可靠性。重连策略采用指数退避算法,避免频繁重连消耗资源。
WebSocket方案的挑战:
- 网络依赖:需要医院内部网络支持,网络中断时推送失败
- 连接维护:长时间连接可能被中间设备(防火墙、NAT)超时断开,需要心跳保活
- 安全性:医疗数据传输需要加密(WSS),且需要身份认证机制
- 扩展性:护士数量增加时,服务端需要支持更多并发连接
8.2 家属端预警推送 — Push Kit
家属端通常不在医院网络内,WebSocket长连接在移动网络环境下不够稳定且耗电。HarmonyOS的Push Kit提供了系统级的推送服务,适合家属端的远程通知推送。
Push Kit架构
患者端设备 IVGuard服务端 Push Kit云服务 家属端设备 │ │ │ │ │ 上报液位预警 │ │ │ │──────────────────→│ │ │ │ │ 调用Push Kit API │ │ │ │─────────────────────→│ │ │ │ │ 推送通知到设备 │ │ │ │────────────────────→│ │ │ │ │ 通知栏弹出 │ │ │ │ +振动
Push Kit集成代码
` ypescript
import { pushService } from ‘@kit.PushKit’
class FamilyPushService {
private static pushToken: string = ‘’
static async init(): Promise {
try {
pushService.on(‘tokenChange’, (token: string) => {
FamilyPushService.pushToken = token
hilog.info(DOMAIN, TAG, Push token received: …)
FamilyPushService.registerTokenToServer(token)
})
await pushService.init({
bundleName: ‘com.ivguard.monitor’
})
} catch (err) {
hilog.error(DOMAIN, TAG, ‘Push kit init failed’)
}
}
private static async registerTokenToServer(token: string): Promise {
const settings = DataStore.loadSettings()
const response = await fetch(‘https://api.ivguard.com/push/register’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({
token: token,
userId: settings.userId,
role: UserRole.FAMILY,
patientId: settings.patientId
})
})
if (!response.ok) {
hilog.error(DOMAIN, TAG, ‘Token registration failed’)
}
}
}
`
服务端推送逻辑
` ypescript
class AlertPushService {
async pushToFamily(patientId: string, alert: AlertMessage): Promise {
const familyTokens = await this.getFamilyTokens(patientId)
for (const token of familyTokens) {
await this.sendPushNotification(token, {
title: this.getAlertTitle(alert.type),
body: this.getAlertBody(alert.type, alert),
data: {
type: alert.type,
patientId: patientId,
timestamp: alert.timestamp
}
})
}
}
private async sendPushNotification(
token: string,
payload: PushPayload
): Promise {
const response = await fetch(‘https://push-api.hihonor.com/v2/push’, {
method: ‘POST’,
headers: {
‘Authorization’: Bearer ,
‘Content-Type’: ‘application/json’
},
body: JSON.stringify({
token: token,
title: payload.title,
body: payload.body,
data: JSON.stringify(payload.data)
})
})
hilog.info(DOMAIN, TAG, Push sent to …)
}
}
`
Push Kit方案的优势:
- 系统级推送:即使应用未运行,系统也能接收并展示推送通知
- 低功耗:不需要维持长连接,由系统推送服务代为管理
- 高到达率:Push Kit有系统级保障,推送到达率远高于应用自建长连接
- 跨网络:不受医院内网限制,家属在任何网络环境下都能收到推送
8.3 分布式软总线方案 — 设备间消息传递
HarmonyOS的分布式软总线(Distributed Soft Bus)提供了设备间直接通信的能力,无需经过云端中转。这在医院内部署时可以利用同一WiFi网络下的设备间直连通信,减少延迟和网络依赖。
分布式软总线架构
患者端设备 分布式软总线 护士站平板 │ │ │ │ 发现同网络设备 │ │ │──────────────────→ │ │ │ │ 设备认证与连接 │ │ │←───────────────────────→│ │ 发送预警消息 │ │ │──────────────────→ │ 转发消息 │ │ │───────────────────────→│ │ │ │ 显示通知
分布式软总线实现
` ypescript
import { distributedDeviceManager } from ‘@kit.DistributedServiceKit’
import { distributedData } from ‘@kit.ArkData’
class DistributedAlertService {
private deviceManager: distributedDeviceManager.DeviceManager | null = null
private kvStore: distributedData.KVStore | null = null
async init(): Promise {
this.deviceManager = distributedDeviceManager.createDeviceManager(‘com.ivguard.monitor’)
const devices = this.deviceManager.getAvailableDeviceListSync()
hilog.info(DOMAIN, TAG, Found distributed devices)
const kvManager = distributedData.createKVManager({
bundleName: 'com.ivguard.monitor',
context: getContext()
})
this.kvStore = await kvManager.getKVStore({
storeId: 'ivguard_alerts',
securityLevel: distributedData.SecurityLevel.S1
})
this.kvStore.on('dataChange', (data: distributedData.ChangeNotification) => {
data.getInsertEntries().forEach((entry) => {
const alert = JSON.parse(entry.value.value) as AlertMessage
this.handleDistributedAlert(alert)
})
})
}
async publishAlert(alert: AlertMessage): Promise {
if (!this.kvStore) {
return
}
const key = alert__
await this.kvStore.put(key, JSON.stringify(alert))
}
private handleDistributedAlert(alert: AlertMessage): void {
switch (alert.type) {
case ‘LEVEL_ALERT’:
NotificationService.sendLevelAlert(alert.bedNo, alert.level!)
break
case ‘COMPLETION’:
NotificationService.sendCompletionNotice(alert.bedNo)
break
default:
break
}
}
}
`
分布式软总线方案的优势:
- 低延迟:设备间直连通信,延迟通常在10ms以内
- 无云端依赖:不依赖互联网连接,适合医院内网环境
- 数据安全:数据不经过云端,减少泄露风险
- 自动发现:同网络下的设备自动发现和连接,无需手动配置
8.4 实现路径:服务端→Push→设备通知
综合以上三种跨设备推送方案,IVGuard的跨设备推送实现路径设计如下:
第一阶段:WebSocket基础版(v2.0)
- 实现患者端到护士端的WebSocket推送
- 建立基础的预警消息协议
- 实现护士端的实时通知显示
- 部署简单的WebSocket服务端
第二阶段:Push Kit扩展版(v2.5)
- 集成HarmonyOS Push Kit
- 实现家属端远程推送
- 建立Push Token管理服务
- 实现推送消息的到达确认机制
第三阶段:分布式软总线增强版(v3.0)
- 利用分布式软总线实现设备间直连
- 实现KVStore共享的预警数据同步
- 支持护士站多设备协同显示
- 实现设备间预警确认和交接
第四阶段:智能路由版(v3.5)
- 根据护士位置和状态智能选择推送通道
- 实现推送优先级排序和聚合
- 支持预警升级机制(未确认的预警自动升级提醒强度)
- 实现护士响应追踪和闭环管理
智能路由的推送策略:
` ypescript
class SmartAlertRouter {
async routeAlert(alert: AlertMessage): Promise {
const nurseStatus = await this.getNurseStatus(alert.bedNo)
if (nurseStatus.isNearby) {
// 护士在附近,使用分布式软总线低延迟推送
await this.distributedPush(alert)
} else if (nurseStatus.isOnline) {
// 护士在线但不在附近,使用WebSocket推送
await this.websocketPush(alert)
} else {
// 护士不在线,使用Push Kit推送
await this.cloudPush(alert)
}
// 同时推送家属端
await this.familyPush(alert)
}
private async checkAlertAcknowledgement(alertId: string): Promise {
// 等待30秒确认
await this.sleep(30000)
const acknowledged = await this.isAcknowledged(alertId)
if (!acknowledged) {
// 未确认,升级提醒强度
await this.escalateAlert(alertId)
}
return acknowledged
}
}
`
预警升级机制
预警升级是跨设备推送的关键功能。当护士未在规定时间内确认预警时,系统自动升级提醒强度:
| 时间 | 升级行为 | 通知方式 |
|---|---|---|
| T+0s | 初始推送 | WebSocket/Push通知 |
| T+30s | 未确认升级 | 振动+声音通知 |
| T+60s | 二次升级 | 电话呼叫 |
| T+120s | 三次升级 | 通知护士长 |
| T+300s | 最终升级 | 全科广播 |
` ypescript
class AlertEscalationService {
private escalationTimers: Map<string, number> = new Map()
async startEscalation(alertId: string, bedNo: string): Promise {
const timer1 = setTimeout(async () => {
if (!(await this.isAcknowledged(alertId))) {
await this.escalateToVibration(bedNo)
}
}, 30000)
const timer2 = setTimeout(async () => {
if (!(await this.isAcknowledged(alertId))) {
await this.escalateToPhoneCall(bedNo)
}
}, 60000)
const timer3 = setTimeout(async () => {
if (!(await this.isAcknowledged(alertId))) {
await this.escalateToHeadNurse(bedNo)
}
}, 120000)
this.escalationTimers.set(alertId, timer1)
}
acknowledgeAlert(alertId: string): void {
const timer = this.escalationTimers.get(alertId)
if (timer) {
clearTimeout(timer)
this.escalationTimers.delete(alertId)
}
this.markAcknowledged(alertId)
}
}
`
8.5 跨设备推送的安全考虑
医疗数据的跨设备传输涉及重要的安全和隐私问题,必须严格遵循相关法规和标准:
数据加密
- 传输加密:所有跨设备通信使用TLS/WSS加密
- 存储加密:预警数据在设备端和服务端均加密存储
- 端到端加密:敏感患者信息使用端到端加密,服务端无法解密
身份认证
- 设备认证:每个设备使用唯一的设备证书进行认证
- 用户认证:护士登录需要双因素认证(密码+指纹/面部识别)
- Token管理:Push Token与用户身份绑定,定期刷新
数据最小化
- 推送消息仅包含必要信息(床号、预警类型、时间戳)
- 患者姓名等敏感信息不在推送消息中传输
- 详细信息需要用户登录后在应用内查看
合规性
- 遵循《个人信息保护法》和《数据安全法》
- 遵循医疗数据安全相关标准(如等保三级)
- 推送服务使用境内云服务,数据不出境
附录
A. NotificationKit API速查
| API | 说明 | 返回类型 |
|---|---|---|
| publish(request) | 发布通知 | Promise |
| cancel(id) | 取消通知 | Promise |
| cancelAll() | 取消所有通知 | Promise |
| isNotificationEnabled() | 检查通知是否启用 | Promise |
| requestEnableNotification() | 请求启用通知 | Promise |
| getAllActiveNotifications() | 获取所有活跃通知 | Promise |
| getActiveNotificationCount() | 获取活跃通知数量 | Promise |
B. AVPlayer状态速查
| 状态 | 可执行操作 | 自动触发条件 |
|---|---|---|
| idle | setFdSrc, setUrlSrc, release | 初始/重置后 |
| initialized | prepare, release | 设置数据源后 |
| prepared | play, release | prepare()后 |
| playing | pause, seek, stop, release | play()后 |
| completed | reset, play, release | 播放完成后 |
C. 振动模式速查
| 模式 | 时长 | 用途 | 触觉描述 |
|---|---|---|---|
| SHORT | 100ms | 流速异常 | 轻点 |
| LONG | 500ms | 低液位预警 | 持续嗡 |
| DOUBLE | 200+200ms | 输液完成 | 嗡-嗡 |
D. 三角色提醒策略速查
| 通道/设置 | 患者 | 护士 | 家属 |
|---|---|---|---|
| 通知栏 | 必需 | 必需 | 必需 |
| 语音 | 默认开启 | 默认关闭 | 不使用 |
| 振动 | 长/双 | 仅Warning短 | 仅完成短 |
| 话术 | 简洁温和 | 专业精确 | 关怀安心 |
| 蓝牙 | 必需 | 必需 | 不需要 |
| 后台音频 | 必需 | 不需要 | 不需要 |
更多推荐


所有评论(0)