HarmonyOS开发:运动检测与活动状态识别

举报
Jack20 发表于 2026/06/21 15:56:24 2026/06/21
【摘要】 HarmonyOS开发:运动检测与活动状态识别核心要点:本文系统讲解基于HarmonyOS多传感器融合的运动检测与活动状态识别技术,涵盖加速度/陀螺仪/重力传感器数据融合、状态机驱动的活动切换、以及基于滑动窗口特征提取的实时运动状态判定方案。项目说明开发语言ArkTS核心API@ohos.sensor (加速度/陀螺仪/重力)、@ohos.activityRecognition 一、背景与...

HarmonyOS开发:运动检测与活动状态识别

核心要点:本文系统讲解基于HarmonyOS多传感器融合的运动检测与活动状态识别技术,涵盖加速度/陀螺仪/重力传感器数据融合、状态机驱动的活动切换、以及基于滑动窗口特征提取的实时运动状态判定方案。

项目 说明
开发语言 ArkTS
核心API @ohos.sensor (加速度/陀螺仪/重力)、@ohos.activityRecognition

一、背景与动机

运动检测(Motion Detection)是智能设备健康生态的基础能力。从简单的"是否在运动"判断,到精确的"正在做什么运动"识别,运动检测技术正在从粗粒度走向细粒度。

HarmonyOS的多传感器融合框架为运动检测提供了丰富的数据源:加速度传感器检测线性运动、陀螺仪检测旋转角速度、重力传感器提供姿态参考。通过融合多种传感器数据,可以实现比单一传感器更精准、更鲁棒的运动状态识别。

典型应用场景

  1. 智能久坐提醒:检测用户长时间静止,自动提醒起身活动
  2. 运动模式自动切换:从步行切换到跑步时自动调整监测参数
  3. 睡眠质量监测:夜间活动检测辅助判断深睡/浅睡阶段
  4. 老人看护:异常活动检测(如长时间不动、异常剧烈运动)
  5. 运动处方执行:监测用户是否按计划完成运动量

二、核心原理

2.1 多传感器数据融合

运动检测的核心挑战是从噪声数据中提取可靠的运动特征。单一传感器容易受到干扰:

传感器 优势 劣势
加速度计 检测线性运动、步数 受重力影响、无法区分倾斜与加速
陀螺仪 检测旋转、姿态变化 存在漂移、长时间累积误差
重力传感器 提供稳定的重力参考 无法检测动态运动
线性加速度 去除重力后的纯运动信号 精度依赖加速度计校准

融合策略:以加速度计为主传感器,陀螺仪辅助姿态校正,重力传感器提供参考基线。

2.2 活动状态分类

本文将活动状态分为以下5类:

┌─────────┐    ┌─────────┐    ┌─────────┐
│  静止    │───→│  微动    │───→│  步行    │
│ STILL   │    │ TINY    │    │ WALK    │
└─────────┘    └─────────┘    └─────────┘
                                   │
                                   ▼
                              ┌─────────┐    ┌─────────┐
                              │  跑步    │───→│  剧烈    │
                              │ RUN     │    │ INTENSE │
                              └─────────┘    └─────────┘

2.3 特征提取方法

从传感器数据中提取以下时域和频域特征:

时域特征

  • 信号能量(Signal Energy):加速度平方和的均值,反映运动强度
  • 过零率(Zero-Crossing Rate):信号穿越基线的频率,反映运动节奏
  • 峰峰值(Peak-to-Peak):最大值与最小值之差,反映运动幅度
  • 标准差(Standard Deviation):反映信号波动程度

频域特征

  • 主频率(Dominant Frequency):FFT后的最大频率分量,区分步行(1-2Hz)和跑步(2-4Hz)
  • 频谱能量分布:低频/高频能量比,辅助判断运动类型

2.4 运动检测算法流程图

flowchart TD
    A[多传感器数据采集] --> B[时间对齐与同步]
    B --> C[加速度+陀螺仪数据融合]
    C --> D[滑动窗口特征提取<br/>窗口2,步长0.5]
    D --> E{特征向量分类}
    
    E -->|能量<0.1 & 标准差<0.05| F[静止 STILL]
    E -->|能量0.1-0.5 & 过零率<2Hz| G[微动 TINY]
    E -->|主频1-2Hz & 对称性>0.8| H[步行 WALK]
    E -->|主频2-4Hz & 能量>1.0| I[跑步 RUN]
    E -->|能量>3.0 & 无规律| J[剧烈 INTENSE]
    
    F & G & H & I & J --> K[状态机平滑过滤]
    K --> L[活动状态输出]
    L --> M[状态变更通知]
    
    classDef inputStyle fill:#1a1a2e,stroke:#e94560,color:#fff,stroke-width:2px
    classDef processStyle fill:#16213e,stroke:#0f3460,color:#e0e0e0,stroke-width:2px
    classDef decisionStyle fill:#0f3460,stroke:#533483,color:#fff,stroke-width:2px
    classDef stateStyle fill:#533483,stroke:#e94560,color:#fff,stroke-width:2px
    classDef outputStyle fill:#e94560,stroke:#ff6b6b,color:#fff,stroke-width:2px
    
    class A,B,C inputStyle
    class D,K processStyle
    class E decisionStyle
    class F,G,H,I,J stateStyle
    class L,M outputStyle

三、代码实战

3.1 多传感器数据融合管理器

实现加速度计、陀螺仪、重力传感器的同步订阅与数据融合:

// MultiSensorManager.ets — 多传感器数据融合管理器
import sensor from '@ohos.sensor';
import { BusinessError } from '@ohos.base';

// 融合后的传感器数据帧
interface SensorFrame {
  timestamp: number;         // 时间戳(毫秒)
  accelX: number;            // 加速度X
  accelY: number;            // 加速度Y
  accelZ: number;            // 加速度Z
  gyroX: number;             // 角速度X
  gyroY: number;             // 角速度Y
  gyroZ: number;             // 角速度Z
  gravityX: number;          // 重力X
  gravityY: number;          // 重力Y
  gravityZ: number;          // 重力Z
  linearAccelMagnitude: number;  // 线性加速度合值
}

// 传感器订阅配置
const MULTI_SENSOR_CONFIG = {
  samplingInterval: 20_000_000,  // 50Hz采样
  fusionTimeout: 50,             // 融合超时50ms
};

@ObservedV2
export class MultiSensorManager {
  // 最新传感器数据
  private latestAccel: sensor.AccelerometerResponse | null = null;
  private latestGyro: sensor.GyroscopeResponse | null = null;
  private latestGravity: sensor.GravitySensorResponse | null = null;

  // 订阅ID列表
  private subscribeIds: number[] = [];

  // 数据回调
  private onFrameCallback?: (frame: SensorFrame) => void;

  // 错误回调
  private onErrorCallback?: (error: BusinessError) => void;

  /**
   * 启动多传感器订阅
   */
  startSensors(
    onFrame: (frame: SensorFrame) => void,
    onError?: (error: BusinessError) => void
  ): void {
    this.onFrameCallback = onFrame;
    this.onErrorCallback = onError;

    // 订阅加速度传感器
    try {
      const accelId = sensor.on(sensor.SensorId.ACCELEROMETER,
        (data: sensor.AccelerometerResponse) => {
          this.latestAccel = data;
          this.tryEmitFrame();
        },
        { interval: MULTI_SENSOR_CONFIG.samplingInterval }
      );
      this.subscribeIds.push(accelId);
    } catch (e) {
      this.onErrorCallback?.(e as BusinessError);
    }

    // 订阅陀螺仪传感器
    try {
      const gyroId = sensor.on(sensor.SensorId.GYROSCOPE,
        (data: sensor.GyroscopeResponse) => {
          this.latestGyro = data;
        },
        { interval: MULTI_SENSOR_CONFIG.samplingInterval }
      );
      this.subscribeIds.push(gyroId);
    } catch (e) {
      this.onErrorCallback?.(e as BusinessError);
    }

    // 订阅重力传感器
    try {
      const gravityId = sensor.on(sensor.SensorId.GRAVITY,
        (data: sensor.GravitySensorResponse) => {
          this.latestGravity = data;
        },
        { interval: MULTI_SENSOR_CONFIG.samplingInterval }
      );
      this.subscribeIds.push(gravityId);
    } catch (e) {
      this.onErrorCallback?.(e as BusinessError);
    }

    console.info('[MultiSensor] 多传感器订阅已启动');
  }

  /**
   * 尝试发射融合数据帧
   * 以加速度计为主时钟源,确保数据对齐
   */
  private tryEmitFrame(): void {
    if (!this.latestAccel) return;

    const accel = this.latestAccel;
    const gyro = this.latestGyro;
    const gravity = this.latestGravity;

    // 计算线性加速度(去除重力分量)
    let linearMag = 0;
    if (gravity) {
      const lx = accel.x - gravity.x;
      const ly = accel.y - gravity.y;
      const lz = accel.z - gravity.z;
      linearMag = Math.sqrt(lx * lx + ly * ly + lz * lz);
    } else {
      // 无重力传感器时,使用高通滤波近似
      linearMag = Math.sqrt(accel.x * accel.x + accel.y * accel.y + accel.z * accel.z) - 9.81;
    }

    const frame: SensorFrame = {
      timestamp: accel.timestamp / 1_000_000, // 纳秒转毫秒
      accelX: accel.x,
      accelY: accel.y,
      accelZ: accel.z,
      gyroX: gyro?.x ?? 0,
      gyroY: gyro?.y ?? 0,
      gyroZ: gyro?.z ?? 0,
      gravityX: gravity?.x ?? 0,
      gravityY: gravity?.y ?? 0,
      gravityZ: gravity?.z ?? 0,
      linearAccelMagnitude: Math.abs(linearMag),
    };

    this.onFrameCallback?.(frame);
  }

  /**
   * 停止所有传感器订阅
   */
  stopSensors(): void {
    // 停止加速度计
    try { sensor.off(sensor.SensorId.ACCELEROMETER); } catch (e) { /* 忽略 */ }
    // 停止陀螺仪
    try { sensor.off(sensor.SensorId.GYROSCOPE); } catch (e) { /* 忽略 */ }
    // 停止重力传感器
    try { sensor.off(sensor.SensorId.GRAVITY); } catch (e) { /* 忽略 */ }

    this.subscribeIds = [];
    this.latestAccel = null;
    this.latestGyro = null;
    this.latestGravity = null;
    console.info('[MultiSensor] 多传感器订阅已停止');
  }
}

3.2 滑动窗口特征提取与活动状态分类

实现基于滑动窗口的特征提取和活动状态分类器:

// ActivityClassifier.ets — 活动状态分类器

// 活动状态枚举
export enum ActivityState {
  STILL = 'STILL',       // 静止
  TINY = 'TINY',         // 微动(坐姿小幅度活动)
  WALK = 'WALK',         // 步行
  RUN = 'RUN',           // 跑步
  INTENSE = 'INTENSE',   // 剧烈运动
}

// 活动状态变更事件
export interface ActivityChangeEvent {
  previousState: ActivityState;
  currentState: ActivityState;
  confidence: number;     // 置信度 0-1
  timestamp: number;
}

// 特征向量
interface FeatureVector {
  energy: number;           // 信号能量
  stdDev: number;           // 标准差
  zeroCrossingRate: number; // 过零率
  peakToPeak: number;       // 峰峰值
  dominantFreq: number;     // 主频率
  gyroEnergy: number;       // 陀螺仪能量
}

// 分类阈值配置
const CLASSIFY_THRESHOLDS = {
  // 静止判定
  stillMaxEnergy: 0.08,
  stillMaxStdDev: 0.05,
  // 微动判定
  tinyMaxEnergy: 0.5,
  tinyMaxZeroCrossing: 2.0,
  // 步行判定
  walkMinFreq: 0.8,
  walkMaxFreq: 2.5,
  walkMinSymmetry: 0.6,
  // 跑步判定
  runMinFreq: 2.0,
  runMaxFreq: 4.5,
  runMinEnergy: 1.0,
  // 剧烈运动
  intenseMinEnergy: 3.0,
};

// 状态机平滑配置
const STATE_MACHINE_CONFIG = {
  minStateDuration: 2000,    // 状态最短持续时间2秒(防抖)
  confirmationCount: 3,      // 连续确认次数
};

@ObservedV2
export class ActivityClassifier {
  // 当前活动状态
  @Trace currentActivity: ActivityState = ActivityState.STILL;
  @Trace confidence: number = 0;

  // 滑动窗口缓冲区
  private windowBuffer: number[] = [];
  private readonly windowSize: number = 100;  // 2秒@50Hz
  private readonly stepSize: number = 25;     // 0.5秒步长

  // 陀螺仪能量缓冲
  private gyroEnergyBuffer: number[] = [];

  // 状态机平滑
  private pendingState: ActivityState = ActivityState.STILL;
  private pendingCount: number = 0;
  private lastStateChangeTime: number = 0;

  // 状态变更回调
  private onStateChangeCallback?: (event: ActivityChangeEvent) => void;

  /**
   * 设置状态变更回调
   */
  setOnStateChangeListener(callback: (event: ActivityChangeEvent) => void): void {
    this.onStateChangeCallback = callback;
  }

  /**
   * 处理传感器数据帧
   */
  processFrame(frame: { linearAccelMagnitude: number; gyroX: number; gyroY: number; gyroZ: number; timestamp: number }): void {
    // 将线性加速度加入滑动窗口
    this.windowBuffer.push(frame.linearAccelMagnitude);
    if (this.windowBuffer.length > this.windowSize) {
      this.windowBuffer.splice(0, this.windowBuffer.length - this.windowSize);
    }

    // 陀螺仪能量
    const gyroEnergy = frame.gyroX * frame.gyroX + frame.gyroY * frame.gyroY + frame.gyroZ * frame.gyroZ;
    this.gyroEnergyBuffer.push(gyroEnergy);
    if (this.gyroEnergyBuffer.length > this.windowSize) {
      this.gyroEnergyBuffer.splice(0, this.gyroEnergyBuffer.length - this.windowSize);
    }

    // 窗口满时进行分类
    if (this.windowBuffer.length >= this.windowSize) {
      const features = this.extractFeatures();
      if (features) {
        const newState = this.classifyActivity(features);
        this.smoothStateTransition(newState, frame.timestamp);
      }
    }
  }

  /**
   * 特征提取
   */
  private extractFeatures(): FeatureVector | null {
    const data = this.windowBuffer;
    if (data.length < this.windowSize) return null;

    // 信号能量(均值平方)
    const mean = data.reduce((a, b) => a + b, 0) / data.length;
    const energy = data.reduce((sum, val) => sum + val * val, 0) / data.length;

    // 标准差
    const variance = data.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / data.length;
    const stdDev = Math.sqrt(variance);

    // 过零率
    let zeroCrossings = 0;
    for (let i = 1; i < data.length; i++) {
      if ((data[i - 1] - mean) * (data[i] - mean) < 0) {
        zeroCrossings++;
      }
    }
    const zeroCrossingRate = zeroCrossings / (data.length / 50); // 每秒过零次数

    // 峰峰值
    const maxVal = Math.max(...data);
    const minVal = Math.min(...data);
    const peakToPeak = maxVal - minVal;

    // 主频率(简化FFT:使用自相关法估计)
    const dominantFreq = this.estimateDominantFrequency(data);

    // 陀螺仪能量
    const gyroEnergy = this.gyroEnergyBuffer.length > 0
      ? this.gyroEnergyBuffer.reduce((a, b) => a + b, 0) / this.gyroEnergyBuffer.length
      : 0;

    return { energy, stdDev, zeroCrossingRate, peakToPeak, dominantFreq, gyroEnergy };
  }

  /**
   * 自相关法估计主频率
   */
  private estimateDominantFrequency(data: number[]): number {
    const sampleRate = 50; // 50Hz
    const minLag = Math.floor(sampleRate / 4.5); // 对应4.5Hz
    const maxLag = Math.floor(sampleRate / 0.5); // 对应0.5Hz

    let maxCorr = -Infinity;
    let bestLag = minLag;

    for (let lag = minLag; lag <= Math.min(maxLag, data.length - 1); lag++) {
      let corr = 0;
      let count = 0;
      for (let i = 0; i < data.length - lag; i++) {
        corr += data[i] * data[i + lag];
        count++;
      }
      corr = count > 0 ? corr / count : 0;

      if (corr > maxCorr) {
        maxCorr = corr;
        bestLag = lag;
      }
    }

    return bestLag > 0 ? sampleRate / bestLag : 0;
  }

  /**
   * 活动状态分类
   */
  private classifyActivity(features: FeatureVector): ActivityState {
    const { energy, stdDev, zeroCrossingRate, dominantFreq, gyroEnergy } = features;

    // 决策树分类
    if (energy < CLASSIFY_THRESHOLDS.stillMaxEnergy && stdDev < CLASSIFY_THRESHOLDS.stillMaxStdDev) {
      return ActivityState.STILL;
    }

    if (energy < CLASSIFY_THRESHOLDS.tinyMaxEnergy && zeroCrossingRate < CLASSIFY_THRESHOLDS.tinyMaxZeroCrossing) {
      return ActivityState.TINY;
    }

    if (energy > CLASSIFY_THRESHOLDS.intenseMinEnergy) {
      return ActivityState.INTENSE;
    }

    if (dominantFreq >= CLASSIFY_THRESHOLDS.runMinFreq &&
        dominantFreq <= CLASSIFY_THRESHOLDS.runMaxFreq &&
        energy >= CLASSIFY_THRESHOLDS.runMinEnergy) {
      return ActivityState.RUN;
    }

    if (dominantFreq >= CLASSIFY_THRESHOLDS.walkMinFreq &&
        dominantFreq <= CLASSIFY_THRESHOLDS.walkMaxFreq) {
      return ActivityState.WALK;
    }

    // 默认:根据能量判断
    if (energy > 1.0) {
      return ActivityState.RUN;
    } else if (energy > 0.3) {
      return ActivityState.WALK;
    } else {
      return ActivityState.TINY;
    }
  }

  /**
   * 状态机平滑过滤(防抖)
   */
  private smoothStateTransition(newState: ActivityState, timestamp: number): void {
    // 状态未变化
    if (newState === this.currentActivity) {
      this.pendingState = newState;
      this.pendingCount = 0;
      this.confidence = 1.0;
      return;
    }

    // 与待确认状态一致,增加计数
    if (newState === this.pendingState) {
      this.pendingCount++;
    } else {
      // 新状态,重置计数
      this.pendingState = newState;
      this.pendingCount = 1;
    }

    // 计算置信度
    this.confidence = Math.min(this.pendingCount / STATE_MACHINE_CONFIG.confirmationCount, 1.0);

    // 满足确认条件
    if (this.pendingCount >= STATE_MACHINE_CONFIG.confirmationCount) {
      // 检查最短持续时间
      const durationSinceLastChange = timestamp - this.lastStateChangeTime;
      if (durationSinceLastChange >= STATE_MACHINE_CONFIG.minStateDuration ||
          this.currentActivity === ActivityState.STILL) {
        // 触发状态变更
        const event: ActivityChangeEvent = {
          previousState: this.currentActivity,
          currentState: newState,
          confidence: this.confidence,
          timestamp: timestamp,
        };

        this.currentActivity = newState;
        this.lastStateChangeTime = timestamp;
        this.pendingCount = 0;

        this.onStateChangeCallback?.(event);
      }
    }
  }

  /**
   * 重置分类器
   */
  reset(): void {
    this.currentActivity = ActivityState.STILL;
    this.confidence = 0;
    this.pendingState = ActivityState.STILL;
    this.pendingCount = 0;
    this.windowBuffer = [];
    this.gyroEnergyBuffer = [];
  }
}

3.3 运动检测完整界面

实现实时运动检测界面,展示活动状态、传感器波形和状态历史:

// MotionDetectPage.ets — 运动检测完整界面
import { MultiSensorManager } from './MultiSensorManager';
import { ActivityClassifier, ActivityState, ActivityChangeEvent } from './ActivityClassifier';

// 活动状态显示配置
interface ActivityDisplayConfig {
  label: string;
  icon: string;
  color: string;
  bgColor: string;
  description: string;
}

const ACTIVITY_CONFIGS: Record<string, ActivityDisplayConfig> = {
  [ActivityState.STILL]: {
    label: '静止', icon: '🧘', color: '#78909C', bgColor: 'rgba(120,144,156,0.15)',
    description: '检测到您当前处于静止状态'
  },
  [ActivityState.TINY]: {
    label: '微动', icon: '👆', color: '#FFB74D', bgColor: 'rgba(255,183,77,0.15)',
    description: '检测到轻微活动,可能是坐姿微调'
  },
  [ActivityState.WALK]: {
    label: '步行', icon: '🚶', color: '#4FC3F7', bgColor: 'rgba(79,195,247,0.15)',
    description: '检测到步行运动,保持良好节奏'
  },
  [ActivityState.RUN]: {
    label: '跑步', icon: '🏃', color: '#00E676', bgColor: 'rgba(0,230,118,0.15)',
    description: '检测到跑步运动,注意控制心率'
  },
  [ActivityState.INTENSE]: {
    label: '剧烈', icon: '⚡', color: '#FF5252', bgColor: 'rgba(255,82,82,0.15)',
    description: '检测到剧烈运动,请注意安全'
  },
};

@Entry
@Component
struct MotionDetectPage {
  // 管理器
  private sensorManager: MultiSensorManager = new MultiSensorManager();
  private classifier: ActivityClassifier = new ActivityClassifier();

  // UI状态
  @State currentActivity: ActivityState = ActivityState.STILL;
  @State confidence: number = 0;
  @State isDetecting: boolean = false;
  @State stillDuration: number = 0;       // 静止持续时间(分钟)
  @State stateHistory: ActivityChangeEvent[] = [];
  @State sensorEnergy: number = 0;

  // 久坐提醒计时
  private stillStartTime: number = 0;
  private stillTimerId: number = -1;
  private readonly SEDENTARY_THRESHOLD: number = 60; // 60分钟久坐阈值

  build() {
    Navigation() {
      Scroll() {
        Column({ space: 20 }) {
          // 当前活动状态展示
          this.CurrentActivitySection()

          // 传感器能量仪表
          this.EnergyGaugeSection()

          // 久坐提醒卡片
          this.SedentaryAlertSection()

          // 状态切换历史
          this.StateHistorySection()

          // 控制按钮
          this.ControlSection()
        }
        .width('100%')
        .padding({ left: 20, right: 20, top: 16, bottom: 40 })
      }
      .scrollBar(BarState.Off)
    }
    .title('运动检测')
    .titleMode(NavigationTitleMode.Mini)
  }

  // ===== 当前活动状态展示 =====
  @Builder
  CurrentActivitySection() {
    Column({ space: 16 }) {
      // 状态图标与名称
      Row({ space: 16 }) {
        // 动态图标
        Text(ACTIVITY_CONFIGS[this.currentActivity].icon)
          .fontSize(48)
          .animation({ duration: 300 })

        Column({ space: 4 }) {
          Text(ACTIVITY_CONFIGS[this.currentActivity].label)
            .fontSize(28)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)

          // 置信度进度条
          Row({ space: 8 }) {
            Text('置信度')
              .fontSize(11)
              .fontColor('#78909C')

            Progress({ value: this.confidence * 100, total: 100, type: ProgressType.Linear })
              .width(120)
              .height(6)
              .color(ACTIVITY_CONFIGS[this.currentActivity].color)
              .backgroundColor('rgba(255,255,255,0.1)')

            Text(`${Math.round(this.confidence * 100)}%`)
              .fontSize(11)
              .fontColor(ACTIVITY_CONFIGS[this.currentActivity].color)
          }
        }

        Blank()

        // 检测状态指示
        if (this.isDetecting) {
          Circle({ width: 10, height: 10 })
            .fill('#00E676')
            .animation({ duration: 500, iterations: -1, curve: Curve.EaseInOut })
        }
      }
      .width('100%')

      // 状态描述
      Text(ACTIVITY_CONFIGS[this.currentActivity].description)
        .fontSize(13)
        .fontColor('#B0BEC5')
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor(ACTIVITY_CONFIGS[this.currentActivity].bgColor)
    }
    .width('100%')
    .padding(20)
    .borderRadius(20)
    .backgroundColor('rgba(15, 52, 96, 0.6)')
    .backdropBlur(20)
  }

  // ===== 传感器能量仪表 =====
  @Builder
  EnergyGaugeSection() {
    Column({ space: 12 }) {
      Text('⚡ 运动能量')
        .fontSize(16)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)

      // 能量条形图
      Row({ space: 4 }) {
        ForEach([0.08, 0.5, 1.0, 3.0, 5.0], (threshold: number) => {
          Column() {
            // 能量柱
            Column()
              .width('100%')
              .height(`${Math.min(this.sensorEnergy / threshold * 30, 60)}%`)
              .backgroundColor(this.sensorEnergy >= threshold ? '#e94560' : 'rgba(233,69,96,0.2)')
              .borderRadius({ topLeft: 4, topRight: 4 })
          }
          .layoutWeight(1)
          .height(60)
          .justifyContent(FlexAlign.End)
        })
      }
      .width('100%')

      // 阈值标签
      Row({ space: 4 }) {
        Text('静止')
          .fontSize(9)
          .fontColor('#546E7A')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('微动')
          .fontSize(9)
          .fontColor('#546E7A')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('步行')
          .fontSize(9)
          .fontColor('#546E7A')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('跑步')
          .fontSize(9)
          .fontColor('#546E7A')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('剧烈')
          .fontSize(9)
          .fontColor('#546E7A')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
      }
      .width('100%')

      // 当前能量值
      Text(`当前能量: ${this.sensorEnergy.toFixed(3)}`)
        .fontSize(12)
        .fontColor('#78909C')
    }
    .width('100%')
    .padding(16)
    .borderRadius(16)
    .backgroundColor('rgba(22, 33, 62, 0.8)')
  }

  // ===== 久坐提醒卡片 =====
  @Builder
  SedentaryAlertSection() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Text('💺')
          .fontSize(20)
        Text('久坐提醒')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
        Blank()
        Text(`${this.stillDuration}分钟`)
          .fontSize(14)
          .fontColor(this.stillDuration >= this.SEDENTARY_THRESHOLD ? '#FF5252' : '#78909C')
      }
      .width('100%')

      // 久坐进度条
      Progress({
        value: Math.min(this.stillDuration, this.SEDENTARY_THRESHOLD),
        total: this.SEDENTARY_THRESHOLD,
        type: ProgressType.Linear
      })
        .width('100%')
        .height(8)
        .color(this.stillDuration >= this.SEDENTARY_THRESHOLD ? '#FF5252' : '#4FC3F7')
        .backgroundColor('rgba(255,255,255,0.1)')

      if (this.stillDuration >= this.SEDENTARY_THRESHOLD) {
        Text('⚠️ 您已久坐超过60分钟,建议起身活动!')
          .fontSize(13)
          .fontColor('#FF5252')
          .fontWeight(FontWeight.Bold)
      } else {
        Text(`距离久坐提醒还有 ${this.SEDENTARY_THRESHOLD - this.stillDuration} 分钟`)
          .fontSize(12)
          .fontColor('#546E7A')
      }
    }
    .width('100%')
    .padding(16)
    .borderRadius(16)
    .backgroundColor('rgba(22, 33, 62, 0.8)')
  }

  // ===== 状态切换历史 =====
  @Builder
  StateHistorySection() {
    Column({ space: 12 }) {
      Text('📋 状态切换历史')
        .fontSize(16)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)

      if (this.stateHistory.length === 0) {
        Text('开始检测后将记录状态变化')
          .fontSize(13)
          .fontColor('#546E7A')
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding(20)
      } else {
        // 最近5条记录
        ForEach(this.stateHistory.slice(-5).reverse(), (event: ActivityChangeEvent) => {
          Row({ space: 12 }) {
            // 时间
            Text(new Date(event.timestamp).toLocaleTimeString())
              .fontSize(11)
              .fontColor('#546E7A')
              .width(70)

            // 状态变更
            Text(ACTIVITY_CONFIGS[event.previousState].label)
              .fontSize(13)
              .fontColor(ACTIVITY_CONFIGS[event.previousState].color)

            Text('→')
              .fontSize(13)
              .fontColor('#546E7A')

            Text(ACTIVITY_CONFIGS[event.currentState].label)
              .fontSize(13)
              .fontColor(ACTIVITY_CONFIGS[event.currentState].color)
              .fontWeight(FontWeight.Bold)

            Blank()

            Text(`${Math.round(event.confidence * 100)}%`)
              .fontSize(11)
              .fontColor('#78909C')
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 })
        })
      }
    }
    .width('100%')
    .padding(16)
    .borderRadius(16)
    .backgroundColor('rgba(22, 33, 62, 0.8)')
  }

  // ===== 控制按钮 =====
  @Builder
  ControlSection() {
    Button(this.isDetecting ? '停止检测' : '开始检测')
      .width('100%')
      .height(52)
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
      .backgroundColor(this.isDetecting ? '#FF5252' : '#e94560')
      .borderRadius(26)
      .onClick(() => this.toggleDetection())
  }

  // ===== 业务逻辑 =====

  private toggleDetection(): void {
    if (this.isDetecting) {
      this.stopDetection();
    } else {
      this.startDetection();
    }
  }

  private startDetection(): void {
    this.isDetecting = true;
    this.stillStartTime = Date.now();

    // 设置状态变更回调
    this.classifier.setOnStateChangeListener((event: ActivityChangeEvent) => {
      this.currentActivity = event.currentState;
      this.confidence = event.confidence;
      this.stateHistory.push(event);

      // 静止状态计时
      if (event.currentState === ActivityState.STILL) {
        this.stillStartTime = Date.now();
        this.startStillTimer();
      } else {
        this.stopStillTimer();
        this.stillDuration = 0;
      }
    });

    // 启动多传感器
    this.sensorManager.startSensors(
      (frame) => {
        this.classifier.processFrame(frame);
        // 更新能量显示
        this.sensorEnergy = frame.linearAccelMagnitude * frame.linearAccelMagnitude;
      },
      (error) => {
        console.error(`[MotionDetect] 传感器错误: ${error.message}`);
      }
    );
  }

  private stopDetection(): void {
    this.isDetecting = false;
    this.sensorManager.stopSensors();
    this.stopStillTimer();
  }

  private startStillTimer(): void {
    this.stopStillTimer();
    this.stillTimerId = setInterval(() => {
      this.stillDuration = Math.floor((Date.now() - this.stillStartTime) / 60000);
    }, 60000) as number;
  }

  private stopStillTimer(): void {
    if (this.stillTimerId !== -1) {
      clearInterval(this.stillTimerId);
      this.stillTimerId = -1;
    }
  }

  aboutToDisappear(): void {
    this.sensorManager.stopSensors();
    this.stopStillTimer();
  }
}

四、踩坑与注意事项

4.1 陀螺仪权限问题

陀螺仪传感器需要额外权限声明:

{
  "requestPermissions": [
    {
      "name": "ohos.permission.ACCELEROMETER",
      "reason": "$string:accelerometer_reason",
      "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
    },
    {
      "name": "ohos.permission.GYROSCOPE",
      "reason": "$string:gyroscope_reason",
      "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
    }
  ]
}

⚠️ 注意:部分低端设备可能不支持陀螺仪传感器,订阅前应先检查传感器可用性:

// 检查传感器是否可用
import sensor from '@ohos.sensor';

const isGyroAvailable = sensor.isSensorSupported(sensor.SensorId.GYROSCOPE);
if (!isGyroAvailable) {
  console.warn('[MotionDetect] 陀螺仪不可用,将使用加速度计降级方案');
}

4.2 传感器数据时间对齐

多传感器数据的时间戳可能不完全同步,需要处理时间对齐:

// ❌ 错误:直接使用不同传感器的时间戳
const accelTime = accelData.timestamp;
const gyroTime = gyroData.timestamp;
// 两者可能相差数十毫秒

// ✅ 正确:以主传感器时间为基准,使用最近一次的辅助传感器数据
// MultiSensorManager中的tryEmitFrame方法已实现此逻辑

4.3 自相关法频率估计的局限性

本文使用的自相关法是一种简化的频率估计方法,存在以下局限:

问题 原因 解决方案
基频倍频 自相关在2倍周期处也有峰值 搜索最小lag的峰值
低频分辨率差 窗口长度限制 增加窗口到4秒
多频率信号 复合运动(如跑步+手臂摆动) 使用FFT进行频谱分析

4.4 状态机防抖参数调优

状态机平滑参数需要根据实际场景调优:

  • minStateDuration:过短会导致频繁切换,过长会延迟状态检测
  • confirmationCount:过少会误判,过多会延迟响应
  • 推荐配置:日常监测2秒/3次,运动场景1秒/2次

4.5 功耗优化

多传感器同时订阅功耗较高,建议:

  1. 静止状态降低采样率:检测到静止后,将采样率从50Hz降至10Hz
  2. 动态启停传感器:静止时只保留加速度计,运动时再启动陀螺仪
  3. 使用系统活动识别API:HarmonyOS提供@ohos.activityRecognition系统级API,功耗更低

五、HarmonyOS 6适配

5.1 系统级活动识别API

HarmonyOS 6增强了系统级活动识别能力:

// HarmonyOS 6:使用系统活动识别服务
import activityRecognition from '@ohos.activityRecognition';

// 订阅活动状态变更
activityRecognition.on('activityChange', {
  minInterval: 5000,  // 最小回调间隔5秒
  activities: [
    activityRecognition.ActivityType.STILL,
    activityRecognition.ActivityType.WALKING,
    activityRecognition.ActivityType.RUNNING,
    activityRecognition.ActivityType.IN_VEHICLE,
    activityRecognition.ActivityType.ON_BICYCLE,
  ]
}, (activities: activityRecognition.ActivityRecognitionResult[]) => {
  for (const activity of activities) {
    console.info(`活动类型: ${activity.type}, 置信度: ${activity.confidence}`);
  }
});

5.2 传感器融合框架增强

HarmonyOS 6提供了更高级的传感器融合API:

// HarmonyOS 6:传感器融合数据直接获取
import sensor from '@ohos.sensor';

// 直接获取线性加速度(系统级融合,无需手动去重力)
sensor.on(sensor.SensorId.LINEAR_ACCELEROMETER, (data) => {
  // data.x, data.y, data.z 已去除重力分量
  const magnitude = Math.sqrt(data.x * data.x + data.y * data.y + data.z * data.z);
  console.info(`线性加速度: ${magnitude}`);
}, { interval: 20_000_000 });

// 直接获取旋转向量(系统级姿态融合)
sensor.on(sensor.SensorId.ROTATION_VECTOR, (data) => {
  // 四元数表示的设备姿态
  console.info(`旋转: x=${data.x}, y=${data.y}, z=${data.z}, w=${data.w}`);
}, { interval: 20_000_000 });

5.3 端侧AI推理加速

HarmonyOS 6支持NNAPI端侧推理,可用于运动分类模型部署:

// HarmonyOS 6:使用端侧AI进行运动分类
import mindSpore from '@ohos.ai.mindSpore';

// 加载预训练的运动分类模型
const model = await mindSpore.loadModelFromFile('models/activity_classify.ms');

// 使用传感器特征向量进行推理
const inputTensor = mindSpore.createTensor(features, [1, 6]); // 6维特征
const output = model.predict([inputTensor]);
const predictedActivity = output[0].getData()[0]; // 分类结果

六、总结

本文完整实现了基于HarmonyOS多传感器融合的运动检测与活动状态识别系统,核心要点如下:

模块 关键技术 要点
多传感器融合 加速度+陀螺仪+重力 以加速度为主时钟源,时间对齐
特征提取 滑动窗口+时频域特征 能量/过零率/主频率多维特征
活动分类 决策树分类器 阈值分层判定,优先低能量状态
状态平滑 状态机防抖 确认次数+最短持续时间双重约束
久坐提醒 静止计时 60分钟阈值,状态切换自动重置

最佳实践建议

  1. 优先使用系统级活动识别API@ohos.activityRecognition),功耗更低、精度更高
  2. 自定义分类场景使用多传感器融合,可获得更细粒度的运动状态
  3. 自相关法适合实时场景,离线分析建议使用FFT获取更精确的频谱
  4. 状态机防抖参数需根据场景调优,运动场景要求更快响应
  5. 功耗优化是关键,静止时降低采样率,运动时动态启停传感器

下一篇文章将深入讲解跌倒检测与紧急求助,实现基于加速度突变的跌倒识别和自动报警机制。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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