HarmonyOS APP开发:跌倒检测与紧急求助

举报
Jack20 发表于 2026/06/21 15:58:42 2026/06/21
【摘要】 HarmonyOS APP开发:跌倒检测与紧急求助核心要点:本文深入讲解基于HarmonyOS加速度传感器的跌倒检测算法实现,涵盖多阶段跌倒判定(失重→冲击→静止)、误报过滤策略、紧急求助通知机制,以及后台长时任务保活方案,为老年人看护提供可靠的技术保障。项目说明开发语言ArkTS核心API@ohos.sensor、@ohos.notification、@ohos.telephony.ca...

HarmonyOS APP开发:跌倒检测与紧急求助

核心要点:本文深入讲解基于HarmonyOS加速度传感器的跌倒检测算法实现,涵盖多阶段跌倒判定(失重→冲击→静止)、误报过滤策略、紧急求助通知机制,以及后台长时任务保活方案,为老年人看护提供可靠的技术保障。

项目 说明
开发语言 ArkTS
核心API @ohos.sensor、@ohos.notification、@ohos.telephony.call、@ohos.backgroundTaskManager

一、背景与动机

跌倒是65岁以上老年人意外伤害的首要原因。据世界卫生组织统计,全球每年约有37.3万人因跌倒死亡,其中绝大多数是65岁以上老人。更严峻的是,跌倒后的"黄金救援时间"通常只有4-6小时——如果老人独居且无法主动求助,后果不堪设想。

智能跌倒检测通过实时监测加速度变化,在检测到跌倒事件后自动触发紧急求助流程,包括:

  1. 振动+声音提醒用户确认
  2. 倒计时结束后自动拨打紧急联系人
  3. 发送包含位置信息的求救短信
  4. 持续发出警报声吸引周围人注意

HarmonyOS的后台长时任务能力确保跌倒检测在应用退到后台后仍可持续运行,而分布式通知机制使得手表检测跌倒后手机同步发出警报。


二、核心原理

2.1 跌倒的物理特征

跌倒过程在加速度信号上呈现明显的三阶段特征:

加速度(m/)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/<br/>持续>200ms}
    B -->|| A
    B -->|| C{检测到冲击阶段<br/>合加速度>20m/<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项敏感权限 全部需要运行时动态授权

最佳实践建议

  1. 三阶段检测是核心,缺少任何阶段都可能导致误报或漏报
  2. 后台保活是关键,跌倒检测必须7×24小时运行
  3. 倒计时确认是必要的,避免误报导致的尴尬和骚扰
  4. 紧急联系人应支持多个,确保至少一个能接通
  5. 位置信息是救命数据,跌倒后第一时间获取GPS坐标
  6. 免责声明不可省略,明确应用仅作为辅助安全工具

下一篇文章将深入讲解运动类型识别,实现步行/跑步/骑行的自动分类与切换。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。