HarmonyOS APP开发:跌倒检测与紧急求助
HarmonyOS APP开发:跌倒检测与紧急求助
核心要点:本文深入讲解基于HarmonyOS加速度传感器的跌倒检测算法实现,涵盖多阶段跌倒判定(失重→冲击→静止)、误报过滤策略、紧急求助通知机制,以及后台长时任务保活方案,为老年人看护提供可靠的技术保障。
| 项目 | 说明 |
|---|---|
| 开发语言 | ArkTS |
| 核心API | @ohos.sensor、@ohos.notification、@ohos.telephony.call、@ohos.backgroundTaskManager |
一、背景与动机
跌倒是65岁以上老年人意外伤害的首要原因。据世界卫生组织统计,全球每年约有37.3万人因跌倒死亡,其中绝大多数是65岁以上老人。更严峻的是,跌倒后的"黄金救援时间"通常只有4-6小时——如果老人独居且无法主动求助,后果不堪设想。
智能跌倒检测通过实时监测加速度变化,在检测到跌倒事件后自动触发紧急求助流程,包括:
- 振动+声音提醒用户确认
- 倒计时结束后自动拨打紧急联系人
- 发送包含位置信息的求救短信
- 持续发出警报声吸引周围人注意
HarmonyOS的后台长时任务能力确保跌倒检测在应用退到后台后仍可持续运行,而分布式通知机制使得手表检测跌倒后手机同步发出警报。
二、核心原理
2.1 跌倒的物理特征
跌倒过程在加速度信号上呈现明显的三阶段特征:
加速度(m/s²)
↑
20 | ┌───┐
| │ │ ← 冲击峰(落地瞬间)
10 | ┌───┘ │
| │ │
0 |──────┘ │
| │
-10 | ↓失重阶段 └──────────── 静止阶段
| (合加速度<1g) (低波动)
└──────────────────────────→ 时间(s)
0.3s 0.1s 2-10s
| 阶段 | 时间 | 加速度特征 | 物理含义 |
|---|---|---|---|
| 失重期 | 0.2-0.5s | 合加速度 < 5 m/s² | 身体开始下落,处于自由落体状态 |
| 冲击期 | 0.05-0.15s | 合加速度 > 20 m/s² | 身体着地,产生剧烈冲击 |
| 静止期 | 2-10s+ | 合加速度 ≈ 9.8 m/s²,低波动 | 跌倒后躺在地上不动 |
2.2 误报过滤策略
跌倒检测最大的挑战是降低误报率。以下场景容易误判为跌倒:
| 误报场景 | 加速度特征 | 区分方法 |
|---|---|---|
| 坐下 | 有冲击但幅度较低 | 冲击峰值 < 15 m/s² |
| 跳跃 | 有失重+冲击但快速恢复 | 冲击后1秒内恢复活动 |
| 手机掉落 | 有冲击但无失重前兆 | 缺少失重阶段 |
| 快速跑步 | 高加速度但持续运动 | 冲击后仍有周期性运动 |
| 上下车 | 有冲击+短暂静止 | 静止期姿态不是水平 |
核心过滤策略:三阶段全部满足才判定为跌倒,任何一阶段不满足即排除。
2.3 紧急求助流程
flowchart TD
A[加速度传感器实时监测] --> B{检测到失重阶段<br/>合加速度<5m/s²<br/>持续>200ms}
B -->|否| A
B -->|是| C{检测到冲击阶段<br/>合加速度>20m/s²<br/>失重后1秒内}
C -->|否| A
C -->|是| D{检测到静止阶段<br/>低波动持续>3秒}
D -->|否| E{冲击后1秒内<br/>恢复运动?}
E -->|是| F[排除:跳跃/坐下]
E -->|否| A
D -->|是| G[⚠️ 疑似跌倒]
G --> H[振动+声音提醒<br/>30秒倒计时]
H --> I{用户点击确认<br/>我没事}
I -->|是| J[取消警报<br/>记录误报]
I -->|否/超时| K[🚨 触发紧急求助]
K --> L[拨打紧急联系人电话]
K --> M[发送位置短信]
K --> N[持续发出警报声]
K --> O[通知紧急联系人APP]
classDef dataStyle fill:#1a1a2e,stroke:#e94560,color:#fff,stroke-width:2px
classDef decisionStyle fill:#0f3460,stroke:#533483,color:#fff,stroke-width:2px
classDef alertStyle fill:#e94560,stroke:#ff6b6b,color:#fff,stroke-width:2px
classDef safeStyle fill:#00C853,stroke:#69F0AE,color:#fff,stroke-width:2px
classDef actionStyle fill:#533483,stroke:#e94560,color:#fff,stroke-width:2px
class A dataStyle
class B,C,D,E decisionStyle
class G,H,K alertStyle
class F,J safeStyle
class L,M,N,O actionStyle
三、代码实战
3.1 三阶段跌倒检测引擎
实现基于三阶段特征匹配的跌倒检测算法:
// FallDetectionEngine.ets — 跌倒检测引擎
import sensor from '@ohos.sensor';
import { BusinessError } from '@ohos.base';
// 跌倒检测阶段
enum FallPhase {
IDLE = 'IDLE', // 空闲(正常活动)
FREE_FALL = 'FREE_FALL', // 失重阶段
IMPACT = 'IMPACT', // 冲击阶段
POST_IMPACT = 'POST_IMPACT', // 冲击后观察期
STATIC = 'STATIC', // 静止阶段
}
// 跌倒事件
export interface FallEvent {
timestamp: number; // 跌倒时间
impactMagnitude: number; // 冲击强度
fallDuration: number; // 跌倒过程持续时间
staticDuration: number; // 静止持续时间
confidence: number; // 置信度0-1
}
// 检测参数配置
const FALL_DETECT_CONFIG = {
// 失重阶段参数
freeFallThreshold: 5.0, // 合加速度阈值(m/s²)
freeFallMinDuration: 150, // 最短持续时间(毫秒)
freeFallMaxDuration: 800, // 最长持续时间(毫秒)
// 冲击阶段参数
impactThreshold: 20.0, // 冲击加速度阈值(m/s²)
impactWindowAfterFall: 1000, // 失重后冲击检测窗口(毫秒)
// 静止阶段参数
staticAccelRange: 2.0, // 静止时加速度波动范围
staticMinDuration: 3000, // 静止最短持续时间(毫秒)
staticMaxDuration: 30000, // 静止最长观察时间(毫秒)
// 采样配置
samplingInterval: 10_000_000, // 10ms采样(100Hz,高频以捕获冲击)
};
@ObservedV2
export class FallDetectionEngine {
// 当前检测阶段
private currentPhase: FallPhase = FallPhase.IDLE;
// 阶段时间戳
private freeFallStartTime: number = 0;
private impactTime: number = 0;
private staticStartTime: number = 0;
// 冲击峰值
private impactPeakValue: number = 0;
// 静止期数据缓冲
private postImpactBuffer: number[] = [];
private readonly postImpactBufferSize: number = 300; // 3秒@100Hz
// 检测状态
@Trace isMonitoring: boolean = false;
@Trace lastFallEvent: FallEvent | null = null;
// 回调
private onFallDetectedCallback?: (event: FallEvent) => void;
private onPhaseChangeCallback?: (phase: FallPhase) => void;
// 订阅ID
private subscribeId: number = -1;
/**
* 设置跌倒检测回调
*/
setOnFallDetected(callback: (event: FallEvent) => void): void {
this.onFallDetectedCallback = callback;
}
/**
* 设置阶段变更回调
*/
setOnPhaseChange(callback: (phase: FallPhase) => void): void {
this.onPhaseChangeCallback = callback;
}
/**
* 启动跌倒检测
*/
startDetection(): void {
if (this.isMonitoring) return;
this.isMonitoring = true;
this.currentPhase = FallPhase.IDLE;
try {
this.subscribeId = sensor.on(sensor.SensorId.ACCELEROMETER,
(data: sensor.AccelerometerResponse) => {
this.processAccelData(data);
},
{ interval: FALL_DETECT_CONFIG.samplingInterval }
);
console.info('[FallDetection] 跌倒检测已启动');
} catch (error) {
const e = error as BusinessError;
console.error(`[FallDetection] 启动失败: ${e.message}`);
this.isMonitoring = false;
}
}
/**
* 停止跌倒检测
*/
stopDetection(): void {
if (!this.isMonitoring) return;
this.isMonitoring = false;
try {
sensor.off(sensor.SensorId.ACCELEROMETER, this.subscribeId);
} catch (e) { /* 忽略 */ }
this.currentPhase = FallPhase.IDLE;
console.info('[FallDetection] 跌倒检测已停止');
}
/**
* 处理加速度数据(核心检测逻辑)
*/
private processAccelData(data: sensor.AccelerometerResponse): void {
// 计算合加速度
const magnitude = Math.sqrt(
data.x * data.x + data.y * data.y + data.z * data.z
);
const currentTime = data.timestamp / 1_000_000; // 纳秒转毫秒
switch (this.currentPhase) {
case FallPhase.IDLE:
this.handleIdlePhase(magnitude, currentTime);
break;
case FallPhase.FREE_FALL:
this.handleFreeFallPhase(magnitude, currentTime);
break;
case FallPhase.IMPACT:
this.handleImpactPhase(magnitude, currentTime);
break;
case FallPhase.POST_IMPACT:
this.handlePostImpactPhase(magnitude, currentTime);
break;
case FallPhase.STATIC:
this.handleStaticPhase(magnitude, currentTime);
break;
}
}
/**
* 空闲阶段:检测失重
*/
private handleIdlePhase(magnitude: number, currentTime: number): void {
if (magnitude < FALL_DETECT_CONFIG.freeFallThreshold) {
// 进入失重阶段
this.currentPhase = FallPhase.FREE_FALL;
this.freeFallStartTime = currentTime;
this.onPhaseChangeCallback?.(FallPhase.FREE_FALL);
}
}
/**
* 失重阶段:等待冲击
*/
private handleFreeFallPhase(magnitude: number, currentTime: number): void {
const elapsed = currentTime - this.freeFallStartTime;
// 失重时间过长,可能是传感器异常,重置
if (elapsed > FALL_DETECT_CONFIG.freeFallMaxDuration) {
this.resetToIdle();
return;
}
// 检测到冲击
if (magnitude > FALL_DETECT_CONFIG.impactThreshold) {
// 验证失重持续时间是否合理
if (elapsed >= FALL_DETECT_CONFIG.freeFallMinDuration) {
this.currentPhase = FallPhase.IMPACT;
this.impactTime = currentTime;
this.impactPeakValue = magnitude;
this.onPhaseChangeCallback?.(FallPhase.IMPACT);
} else {
// 失重时间太短,可能是跳跃等误报
this.resetToIdle();
}
}
// 失重阶段恢复正常加速度(未检测到冲击)
if (magnitude > 8.0 && magnitude < 12.0 && elapsed > 100) {
this.resetToIdle();
}
}
/**
* 冲击阶段:记录峰值后进入观察期
*/
private handleImpactPhase(magnitude: number, currentTime: number): void {
// 更新冲击峰值
if (magnitude > this.impactPeakValue) {
this.impactPeakValue = magnitude;
}
// 冲击后200ms进入观察期
if (currentTime - this.impactTime > 200) {
this.currentPhase = FallPhase.POST_IMPACT;
this.postImpactBuffer = [];
this.onPhaseChangeCallback?.(FallPhase.POST_IMPACT);
}
}
/**
* 冲击后观察期:检测是否静止
*/
private handlePostImpactPhase(magnitude: number, currentTime: number): void {
this.postImpactBuffer.push(magnitude);
const elapsed = currentTime - this.impactTime;
// 超时未静止,排除跌倒
if (elapsed > FALL_DETECT_CONFIG.impactWindowAfterFall) {
this.resetToIdle();
return;
}
// 检查是否进入静止状态(连续0.5秒低波动)
if (this.postImpactBuffer.length >= 50) {
const recentData = this.postImpactBuffer.slice(-50);
const maxVal = Math.max(...recentData);
const minVal = Math.min(...recentData);
const range = maxVal - minVal;
if (range < FALL_DETECT_CONFIG.staticAccelRange) {
// 进入静止阶段
this.currentPhase = FallPhase.STATIC;
this.staticStartTime = currentTime;
this.onPhaseChangeCallback?.(FallPhase.STATIC);
}
}
}
/**
* 静止阶段:确认跌倒
*/
private handleStaticPhase(magnitude: number, currentTime: number): void {
const staticDuration = currentTime - this.staticStartTime;
// 静止超过最短时间,确认跌倒
if (staticDuration >= FALL_DETECT_CONFIG.staticMinDuration) {
const fallEvent: FallEvent = {
timestamp: this.freeFallStartTime,
impactMagnitude: this.impactPeakValue,
fallDuration: this.impactTime - this.freeFallStartTime,
staticDuration: staticDuration,
confidence: this.calculateConfidence(),
};
this.lastFallEvent = fallEvent;
this.onFallDetectedCallback?.(fallEvent);
// 重置检测状态
this.resetToIdle();
return;
}
// 静止期间检测到运动,可能是短暂静止后恢复
if (Math.abs(magnitude - 9.81) > FALL_DETECT_CONFIG.staticAccelRange) {
// 检查是否只是短暂波动
this.postImpactBuffer.push(magnitude);
if (this.postImpactBuffer.length > 20) {
const recentData = this.postImpactBuffer.slice(-20);
const avgDeviation = recentData.reduce((sum, val) =>
sum + Math.abs(val - 9.81), 0) / recentData.length;
if (avgDeviation > 3.0) {
// 明显恢复运动,排除跌倒
this.resetToIdle();
}
}
}
// 静止超时
if (staticDuration > FALL_DETECT_CONFIG.staticMaxDuration) {
this.resetToIdle();
}
}
/**
* 计算跌倒置信度
*/
private calculateConfidence(): number {
let confidence = 0.5; // 基础置信度
// 冲击强度加分(20-40 m/s²范围内,越强越可信)
if (this.impactPeakValue > 25) confidence += 0.2;
if (this.impactPeakValue > 30) confidence += 0.1;
// 失重-冲击时间间隔合理加分
const fallDuration = this.impactTime - this.freeFallStartTime;
if (fallDuration >= 200 && fallDuration <= 600) confidence += 0.1;
// 静止持续时间加分
if (this.lastFallEvent && this.lastFallEvent.staticDuration > 5000) {
confidence += 0.1;
}
return Math.min(confidence, 1.0);
}
/**
* 重置到空闲状态
*/
private resetToIdle(): void {
this.currentPhase = FallPhase.IDLE;
this.freeFallStartTime = 0;
this.impactTime = 0;
this.staticStartTime = 0;
this.impactPeakValue = 0;
this.postImpactBuffer = [];
this.onPhaseChangeCallback?.(FallPhase.IDLE);
}
}
3.2 紧急求助管理器
实现跌倒确认后的紧急求助流程,包括倒计时、电话拨打、短信发送和通知推送:
// EmergencyManager.ets — 紧急求助管理器
import notification from '@ohos.notification';
import notificationManager from '@ohos.notificationManager';
import call from '@ohos.telephony.call';
import geoLocationManager from '@ohos.geoLocationManager';
import vibrator from '@ohos.vibrator';
import { BusinessError } from '@ohos.base';
// 紧急联系人
export interface EmergencyContact {
name: string;
phone: string;
relationship: string; // 关系:配偶/子女/朋友/医生
}
// 求助状态
export enum EmergencyState {
IDLE = 'IDLE',
COUNTDOWN = 'COUNTDOWN', // 倒计时中
ALERTING = 'ALERTING', // 正在求助
CANCELLED = 'CANCELLED', // 用户取消
}
// 默认紧急联系人(应从持久化存储读取)
const DEFAULT_CONTACTS: EmergencyContact[] = [
{ name: '张三', phone: '13800138001', relationship: '子女' },
{ name: '李四', phone: '13800138002', relationship: '邻居' },
];
const EMERGENCY_CONFIG = {
countdownDuration: 30, // 倒计时30秒
alertSoundInterval: 3000, // 警报声间隔3秒
maxCallRetries: 3, // 最大拨号重试次数
smsContent: '【紧急求助】检测到跌倒事件,请尽快确认安全!位置:',
};
@ObservedV2
export class EmergencyManager {
// 紧急联系人列表
@Trace contacts: EmergencyContact[] = DEFAULT_CONTACTS;
// 求助状态
@Trace emergencyState: EmergencyState = EmergencyState.IDLE;
@Trace countdownValue: number = 0;
// 倒计时定时器
private countdownTimerId: number = -1;
// 当前位置
private currentLocation: string = '未知';
// 回调
private onStateChangeCallback?: (state: EmergencyState) => void;
/**
* 触发紧急求助流程
*/
async triggerEmergency(): Promise<void> {
console.info('[Emergency] 触发紧急求助流程');
// 获取当前位置
await this.updateLocation();
// 进入倒计时
this.emergencyState = EmergencyState.COUNTDOWN;
this.countdownValue = EMERGENCY_CONFIG.countdownDuration;
this.onStateChangeCallback?.(EmergencyState.COUNTDOWN);
// 振动提醒
this.startAlertVibration();
// 启动倒计时
this.countdownTimerId = setInterval(() => {
this.countdownValue--;
if (this.countdownValue <= 0) {
// 倒计时结束,执行求助
this.executeEmergency();
}
}, 1000) as number;
}
/**
* 用户确认安全,取消求助
*/
cancelEmergency(): void {
console.info('[Emergency] 用户确认安全,取消求助');
this.emergencyState = EmergencyState.CANCELLED;
this.countdownValue = 0;
// 停止倒计时
if (this.countdownTimerId !== -1) {
clearInterval(this.countdownTimerId);
this.countdownTimerId = -1;
}
// 停止振动
try {
vibrator.stop(vibrator.VibratorStopMode.VIBRATOR_TRIGGERS_MODE);
} catch (e) { /* 忽略 */ }
this.onStateChangeCallback?.(EmergencyState.CANCELLED);
// 3秒后重置状态
setTimeout(() => {
this.emergencyState = EmergencyState.IDLE;
}, 3000);
}
/**
* 执行紧急求助
*/
private async executeEmergency(): void {
console.info('[Emergency] 执行紧急求助');
this.emergencyState = EmergencyState.ALERTING;
this.onStateChangeCallback?.(EmergencyState.ALERTING);
// 停止倒计时
if (this.countdownTimerId !== -1) {
clearInterval(this.countdownTimerId);
this.countdownTimerId = -1;
}
// 1. 发送紧急通知
this.sendEmergencyNotification();
// 2. 拨打紧急联系人电话
await this.callEmergencyContact();
// 3. 发送位置短信
this.sendEmergencySMS();
}
/**
* 发送紧急通知
*/
private sendEmergencyNotification(): void {
try {
const notificationRequest: notification.NotificationRequest = {
id: 9999,
content: {
notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_FULL_TEXT,
fullText: {
title: '🚨 跌倒紧急求助',
text: `检测到跌倒事件!位置:${this.currentLocation}`,
additionalText: new Date().toLocaleTimeString(),
},
},
actionButtons: [
{ title: '确认安全', wantAgent: undefined },
],
isOngoing: true, // 常驻通知
isUnremovable: true, // 不可移除
};
notificationManager.publish(notificationRequest);
console.info('[Emergency] 紧急通知已发送');
} catch (e) {
const error = e as BusinessError;
console.error(`[Emergency] 通知发送失败: ${error.message}`);
}
}
/**
* 拨打紧急联系人电话
*/
private async callEmergencyContact(): Promise<void> {
for (const contact of this.contacts) {
for (let retry = 0; retry < EMERGENCY_CONFIG.maxCallRetries; retry++) {
try {
await call.dial(contact.phone);
console.info(`[Emergency] 已拨打 ${contact.name}(${contact.phone})`);
// 等待30秒后拨打下一个联系人
await this.delay(30000);
break;
} catch (e) {
const error = e as BusinessError;
console.error(`[Emergency] 拨号失败(重试${retry + 1}): ${error.message}`);
await this.delay(5000);
}
}
}
}
/**
* 发送紧急短信
*/
private sendEmergencySMS(): void {
// 注意:发送短信需要ohos.permission.SEND_MESSAGES权限
// 此处使用通知替代,实际生产应使用短信API
const message = `${EMERGENCY_CONFIG.smsContent}${this.currentLocation}`;
console.info(`[Emergency] 短信内容: ${message}`);
// TODO: 调用短信API发送
}
/**
* 获取当前位置
*/
private async updateLocation(): Promise<void> {
try {
const location = await geoLocationManager.getCurrentLocation({
priority: geoLocationManager.LocationRequestPriority.FIXED_POINT,
scenario: geoLocationManager.LocationRequestScenario.UNSET,
});
this.currentLocation = `${location.latitude.toFixed(4)}, ${location.longitude.toFixed(4)}`;
} catch (e) {
this.currentLocation = '位置获取失败';
}
}
/**
* 启动警报振动
*/
private startAlertVibration(): void {
try {
// 间歇振动模式:振动500ms-暂停500ms-振动500ms
vibrator.startVibration({
type: 'pattern',
pattern: [0, 500, 500, 500, 500, 500],
}, {
id: 0,
count: 10, // 重复10次
});
} catch (e) { /* 忽略 */ }
}
/**
* 延迟工具方法
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 设置状态变更回调
*/
setOnStateChangeListener(callback: (state: EmergencyState) => void): void {
this.onStateChangeCallback = callback;
}
}
3.3 跌倒检测完整界面
实现跌倒检测的完整UI,包含检测状态、倒计时确认、紧急求助界面:
// FallDetectionPage.ets — 跌倒检测完整界面
import { FallDetectionEngine, FallEvent } from './FallDetectionEngine';
import { EmergencyManager, EmergencyState, EmergencyContact } from './EmergencyManager';
import backgroundTaskManager from '@ohos.backgroundTaskManager';
import wantAgent from '@ohos.app.ability.wantAgent';
import { BusinessError } from '@ohos.base';
@Entry
@Component
struct FallDetectionPage {
// 引擎与管理器
private detectionEngine: FallDetectionEngine = new FallDetectionEngine();
private emergencyManager: EmergencyManager = new EmergencyManager();
// UI状态
@State isMonitoring: boolean = false;
@State currentPhase: string = 'IDLE';
@State emergencyState: EmergencyState = EmergencyState.IDLE;
@State countdownValue: number = 0;
@State lastFallEvent: FallEvent | null = null;
@State fallHistory: FallEvent[] = [];
@State showCountdownDialog: boolean = false;
// 后台任务ID
private backgroundTaskId: number = -1;
build() {
Navigation() {
Scroll() {
Column({ space: 20 }) {
// 检测状态卡片
this.DetectionStatusCard()
// 跌倒事件记录
this.FallHistorySection()
// 紧急联系人管理
this.EmergencyContactsSection()
// 安全提示
this.SafetyTipsSection()
// 控制按钮
this.ControlSection()
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 40 })
}
.scrollBar(BarState.Off)
}
.title('跌倒检测与紧急求助')
.titleMode(NavigationTitleMode.Mini)
// 跌倒确认对话框
.bindContentCover(this.showCountdownDialog, this.CountdownCover(), {
modalTransition: ModalTransition.NONE,
})
}
// ===== 检测状态卡片 =====
@Builder
DetectionStatusCard() {
Column({ space: 16 }) {
// 状态指示
Row({ space: 12 }) {
Circle({ width: 12, height: 12 })
.fill(this.isMonitoring ? '#00E676' : '#FF5252')
.animation({ duration: 300 })
Text(this.isMonitoring ? '跌倒检测运行中' : '跌倒检测未启动')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
// 当前检测阶段
if (this.isMonitoring) {
Row({ space: 16 }) {
this.PhaseIndicator('空闲', this.currentPhase === 'IDLE', '#78909C')
Text('→').fontColor('#546E7A').fontSize(12)
this.PhaseIndicator('失重', this.currentPhase === 'FREE_FALL', '#FFB74D')
Text('→').fontColor('#546E7A').fontSize(12)
this.PhaseIndicator('冲击', this.currentPhase === 'IMPACT', '#FF5252')
Text('→').fontColor('#546E7A').fontSize(12)
this.PhaseIndicator('静止', this.currentPhase === 'STATIC', '#e94560')
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
// 最近跌倒事件
if (this.lastFallEvent) {
Column({ space: 8 }) {
Text('⚠️ 最近检测事件')
.fontSize(13)
.fontColor('#FF5252')
.fontWeight(FontWeight.Bold)
Row({ space: 12 }) {
Text(`冲击强度: ${this.lastFallEvent.impactMagnitude.toFixed(1)} m/s²`)
.fontSize(12)
.fontColor('#B0BEC5')
Text(`置信度: ${Math.round(this.lastFallEvent.confidence * 100)}%`)
.fontSize(12)
.fontColor('#B0BEC5')
}
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor('rgba(255, 82, 82, 0.15)')
}
}
.width('100%')
.padding(20)
.borderRadius(20)
.backgroundColor('rgba(15, 52, 96, 0.6)')
.backdropBlur(20)
}
@Builder
PhaseIndicator(label: string, active: boolean, color: string) {
Column({ space: 4 }) {
Circle({ width: 8, height: 8 })
.fill(active ? color : 'rgba(255,255,255,0.1)')
Text(label)
.fontSize(10)
.fontColor(active ? color : '#546E7A')
}
}
// ===== 跌倒事件历史 =====
@Builder
FallHistorySection() {
Column({ space: 12 }) {
Text('📋 检测记录')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
if (this.fallHistory.length === 0) {
Text('暂无检测记录')
.fontSize(13)
.fontColor('#546E7A')
.width('100%')
.textAlign(TextAlign.Center)
.padding(20)
} else {
ForEach(this.fallHistory.slice(-5).reverse(), (event: FallEvent, index: number) => {
Row({ space: 12 }) {
Text(`#${this.fallHistory.length - (index as number)}`)
.fontSize(11)
.fontColor('#546E7A')
.width(30)
Column({ space: 4 }) {
Text(new Date(event.timestamp).toLocaleString())
.fontSize(13)
.fontColor('#B0BEC5')
Text(`冲击: ${event.impactMagnitude.toFixed(1)}m/s² | 置信度: ${Math.round(event.confidence * 100)}%`)
.fontSize(11)
.fontColor('#78909C')
}
.layoutWeight(1)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.borderRadius(8)
.backgroundColor('rgba(22, 33, 62, 0.5)')
.padding(12)
})
}
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
// ===== 紧急联系人 =====
@Builder
EmergencyContactsSection() {
Column({ space: 12 }) {
Text('📞 紧急联系人')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
ForEach(this.emergencyManager.contacts, (contact: EmergencyContact) => {
Row({ space: 12 }) {
Circle({ width: 36, height: 36 })
.fill('rgba(233, 69, 96, 0.2)')
.overlay(Text(contact.name.charAt(0)).fontSize(16).fontColor('#e94560'))
Column({ space: 2 }) {
Text(contact.name)
.fontSize(14)
.fontColor('#FFFFFF')
Text(`${contact.relationship} | ${contact.phone}`)
.fontSize(11)
.fontColor('#78909C')
}
.layoutWeight(1)
Text('📞')
.fontSize(20)
.onClick(() => {
// 快速拨号
import('ohos.telephony.call').then(call => {
call.dial(contact.phone);
});
})
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor('rgba(22, 33, 62, 0.5)')
})
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
// ===== 安全提示 =====
@Builder
SafetyTipsSection() {
Column({ space: 8 }) {
Text('💡 安全提示')
.fontSize(14)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Text('• 跌倒检测仅作为辅助安全工具,不能替代人工看护')
.fontSize(12)
.fontColor('#78909C')
Text('• 请确保紧急联系人电话正确且可接通')
.fontSize(12)
.fontColor('#78909C')
Text('• 建议佩戴设备(手表/手环)以获得更准确的检测')
.fontSize(12)
.fontColor('#78909C')
Text('• 剧烈运动可能触发误报,运动时可暂时关闭检测')
.fontSize(12)
.fontColor('#78909C')
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(83, 52, 131, 0.3)')
}
// ===== 控制按钮 =====
@Builder
ControlSection() {
Row({ space: 12 }) {
Button(this.isMonitoring ? '停止检测' : '启动检测')
.layoutWeight(1)
.height(52)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor(this.isMonitoring ? '#FF5252' : '#e94560')
.borderRadius(26)
.onClick(() => this.toggleDetection())
// 测试按钮(模拟跌倒)
Button('模拟测试')
.width(100)
.height(52)
.fontSize(13)
.fontColor('#78909C')
.backgroundColor('rgba(22, 33, 62, 0.8)')
.borderRadius(26)
.onClick(() => this.testFallDetection())
}
.width('100%')
}
// ===== 倒计时确认覆盖层 =====
@Builder
CountdownCover() {
Column({ space: 24 }) {
Text('🚨')
.fontSize(64)
Text('疑似跌倒检测')
.fontSize(24)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Text('如果您安全,请点击下方按钮取消求助')
.fontSize(14)
.fontColor('#B0BEC5')
// 倒计时圆环
Stack() {
Circle()
.width(160)
.height(160)
.fill('transparent')
.stroke('rgba(255,255,255,0.1)')
.strokeWidth(8)
Circle()
.width(160)
.height(160)
.fill('transparent')
.stroke('#FF5252')
.strokeWidth(8)
Text(`${this.countdownValue}`)
.fontSize(48)
.fontColor('#FF5252')
.fontWeight(FontWeight.Bold)
}
// 确认安全按钮
Button('我没事,取消求助')
.width('80%')
.height(56)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor('#00C853')
.borderRadius(28)
.onClick(() => {
this.emergencyManager.cancelEmergency();
this.showCountdownDialog = false;
})
// 立即求助按钮
Button('我需要帮助!')
.width('80%')
.height(48)
.fontSize(15)
.fontColor('#FF5252')
.backgroundColor('rgba(255, 82, 82, 0.15)')
.borderRadius(24)
.onClick(() => {
this.countdownValue = 0;
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('rgba(0, 0, 0, 0.9)')
.backdropBlur(30)
}
// ===== 业务逻辑 =====
private toggleDetection(): void {
if (this.isMonitoring) {
this.stopDetection();
} else {
this.startDetection();
}
}
private startDetection(): void {
this.isMonitoring = true;
// 申请后台长时任务
this.requestBackgroundTask();
// 设置跌倒检测回调
this.detectionEngine.setOnFallDetected((event: FallEvent) => {
console.info(`[FallDetectionPage] 检测到跌倒!置信度: ${event.confidence}`);
this.lastFallEvent = event;
this.fallHistory.push(event);
// 触发紧急求助流程
this.showCountdownDialog = true;
this.emergencyManager.triggerEmergency();
});
// 设置阶段变更回调
this.detectionEngine.setOnPhaseChange((phase) => {
this.currentPhase = phase;
});
// 设置紧急状态回调
this.emergencyManager.setOnStateChangeListener((state) => {
this.emergencyState = state;
if (state === EmergencyState.ALERTING || state === EmergencyState.CANCELLED) {
this.showCountdownDialog = false;
}
});
// 监听倒计时值变化
setInterval(() => {
this.countdownValue = this.emergencyManager.countdownValue;
}, 500);
// 启动检测
this.detectionEngine.startDetection();
}
private stopDetection(): void {
this.isMonitoring = false;
this.detectionEngine.stopDetection();
this.cancelBackgroundTask();
}
/**
* 申请后台长时任务(保活跌倒检测)
*/
private async requestBackgroundTask(): Promise<void> {
try {
const wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [{ bundleName: 'com.example.falldetection', abilityName: 'EntryAbility' }],
operationType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG],
};
const agent = await wantAgent.getWantAgent(wantAgentInfo);
this.backgroundTaskId = await backgroundTaskManager.startBackgroundRunning(
'com.example.falldetection',
backgroundTaskManager.BackgroundMode.DATA_TRANSFER,
agent
);
console.info('[FallDetection] 后台任务已申请');
} catch (e) {
const error = e as BusinessError;
console.error(`[FallDetection] 后台任务申请失败: ${error.message}`);
}
}
/**
* 取消后台长时任务
*/
private cancelBackgroundTask(): void {
if (this.backgroundTaskId !== -1) {
try {
backgroundTaskManager.stopBackgroundRunning('com.example.falldetection');
this.backgroundTaskId = -1;
} catch (e) { /* 忽略 */ }
}
}
/**
* 模拟跌倒测试
*/
private testFallDetection(): void {
const testEvent: FallEvent = {
timestamp: Date.now(),
impactMagnitude: 28.5,
fallDuration: 350,
staticDuration: 5000,
confidence: 0.85,
};
this.lastFallEvent = testEvent;
this.fallHistory.push(testEvent);
this.showCountdownDialog = true;
this.emergencyManager.triggerEmergency();
}
aboutToDisappear(): void {
this.detectionEngine.stopDetection();
this.cancelBackgroundTask();
}
}
四、踩坑与注意事项
4.1 权限清单
跌倒检测功能需要多项敏感权限,务必在module.json5中完整声明:
{
"requestPermissions": [
{
"name": "ohos.permission.ACCELEROMETER",
"reason": "$string:accelerometer_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.LOCATION",
"reason": "$string:location_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:location_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.NOTIFICATION_CONTROLLER",
"reason": "$string:notification_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.CALL_PHONE",
"reason": "$string:call_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.VIBRATE",
"reason": "$string:vibrate_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
"reason": "$string:background_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "always" }
}
]
}
4.2 后台保活策略
跌倒检测必须后台持续运行,但HarmonyOS对后台任务有严格限制:
| 后台模式 | 适用场景 | 限制 |
|---|---|---|
| DATA_TRANSFER | 数据传输 | 需要持续网络活动 |
| AUDIO_PLAYBACK | 音频播放 | 需要实际播放音频 |
| LOCATION | 定位 | 需要持续定位 |
| BLUETOOTH_INTERACTION | 蓝牙 | 需要蓝牙连接 |
推荐方案:使用
BLUETOOTH_INTERACTION模式(配合手表蓝牙连接),或AUDIO_PLAYBACK模式(播放静音音频保活)。
4.3 采样率选择
跌倒检测需要高频采样以捕获冲击峰值:
- 100Hz:推荐,能准确捕获20-50ms的冲击脉冲
- 50Hz:最低可接受,可能漏检短脉冲冲击
- 200Hz:更精确但功耗更高,手表设备慎用
4.4 误报率与漏报率权衡
| 策略 | 误报率 | 漏报率 | 适用场景 |
|---|---|---|---|
| 严格阈值(高冲击+长失重+长静止) | 低 | 较高 | 日常佩戴 |
| 宽松阈值(低冲击+短失重+短静止) | 较高 | 低 | 高风险老人 |
| 自适应阈值(根据活动状态调整) | 中 | 低 | 通用方案 |
4.5 法律与伦理考量
- 跌倒检测不能替代专业医疗看护
- 紧急求助功能不能保证100%送达
- 用户数据(位置、健康信息)需加密存储和传输
- 应用需明确免责声明和隐私政策
五、HarmonyOS 6适配
5.1 系统级跌倒检测API
HarmonyOS 6预计引入系统级跌倒检测服务:
// HarmonyOS 6(预期):系统级跌倒检测
import healthService from '@ohos.healthService';
// 订阅系统跌倒检测事件
healthService.on('fallDetected', (event) => {
console.info(`系统跌倒检测: ${event.timestamp}`);
console.info(`冲击强度: ${event.impactMagnitude}`);
console.info(`置信度: ${event.confidence}`);
// 系统已自动触发紧急求助流程
});
5.2 分布式紧急求助
HarmonyOS 6增强分布式能力,手表检测跌倒后可联动手机、平板:
// HarmonyOS 6:分布式紧急求助
import distributedDeviceManager from '@ohos.distributedDeviceManager';
// 获取可信设备列表
const devices = distributedDeviceManager.getAvailableDeviceListSync();
// 向所有可信设备发送紧急通知
for (const device of devices) {
distributedNotificationManager.publish({
deviceId: device.deviceId,
content: {
notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_FULL_TEXT,
fullText: {
title: '🚨 跌倒紧急求助',
text: '检测到跌倒事件,请确认安全!',
},
},
});
}
5.3 AI辅助跌倒判定
HarmonyOS 6的NNAPI支持端侧AI推理,可用于降低误报率:
// HarmonyOS 6:AI辅助跌倒判定
import mindSpore from '@ohos.ai.mindSpore';
// 加载跌倒检测AI模型
const model = await mindSpore.loadModelFromBuffer(fallDetectModelBuffer);
// 使用加速度序列进行推理
const inputTensor = mindSpore.createTensor(accelSequence, [1, 100, 3]); // 100帧×3轴
const output = model.predict([inputTensor]);
const fallProbability = output[0].getData()[0]; // 跌倒概率0-1
if (fallProbability > 0.8) {
// 高概率跌倒,触发紧急求助
this.triggerEmergency();
}
六、总结
本文完整实现了基于HarmonyOS的跌倒检测与紧急求助系统,核心要点如下:
| 模块 | 关键技术 | 要点 |
|---|---|---|
| 三阶段检测 | 失重→冲击→静止 | 阶段状态机驱动,严格时序约束 |
| 误报过滤 | 多条件联合判定 | 冲击阈值+失重时长+静止时长 |
| 紧急求助 | 倒计时+拨号+短信 | 30秒倒计时,用户可取消 |
| 后台保活 | 长时任务+通知保活 | 选择合适的BackgroundMode |
| 权限管理 | 7项敏感权限 | 全部需要运行时动态授权 |
最佳实践建议:
- 三阶段检测是核心,缺少任何阶段都可能导致误报或漏报
- 后台保活是关键,跌倒检测必须7×24小时运行
- 倒计时确认是必要的,避免误报导致的尴尬和骚扰
- 紧急联系人应支持多个,确保至少一个能接通
- 位置信息是救命数据,跌倒后第一时间获取GPS坐标
- 免责声明不可省略,明确应用仅作为辅助安全工具
下一篇文章将深入讲解运动类型识别,实现步行/跑步/骑行的自动分类与切换。
- 点赞
- 收藏
- 关注作者
评论(0)