💡

HarmonyOS運動開発:室内運動における距離、速度、歩幅の正確な推定

に公開

前言

室内運動の場面では、GPSシグナルが不足しているため、衛星測位に基づく従来の運動データ追跡方法を使用することができません。したがって、室内運動の距離、速度、歩幅を正確に推定する方法は、運動アプリの開発において重要な課題の1つとなっています。この記事では、鴻蒙(HarmonyOS)開発の実践経験を基に、加速度センサーなどのデバイス機能を使用して、室内運動データを正確に推定する方法を詳しく解析します。

一、加速度センサー:室内運動データの核心

加速度センサーは、室内運動データを推定するための鍵となるハードウェアです。このセンサーは、デバイスが3つの軸方向における加速度の変化をリアルタイムで監視することができ、運動状態分析のための基本データを提供します。以下が加速度センサー サービス クラスのコアコードです。

import common from '@ohos.app.ability.common';
import sensor from '@ohos.sensor';
import { BusinessError } from '@kit.BasicServicesKit';
import { abilityAccessCtrl } from '@kit.AbilityKit';
import { UserProfile } from '../user/UserProfile';

interface Accelerometer {
    x: number;
    y: number;
    z: number;
}

export class AccelerationSensorService {
    private static instance: AccelerationSensorService | null = null;
    private context: common.UIAbilityContext;
    private isMonitoring: boolean = false; // 監視中かどうか

    private constructor(context: common.UIAbilityContext) {
        this.context = context;
    }

    static getInstance(context: common.UIAbilityContext): AccelerationSensorService {
        if (!AccelerationSensorService.instance) {
            AccelerationSensorService.instance = new AccelerationSensorService(context);
        }
        return AccelerationSensorService.instance;
    }

    private accelerometerCallback = (data: sensor.AccelerometerResponse) => {
        this.accelerationData = {
            x: data.x,
            y: data.y,
            z: data.z
        };
    };

    private async requestAccelerationPermission(): Promise<boolean> {
        const atManager = abilityAccessCtrl.createAtManager();
        try {
            const result = await atManager.requestPermissionsFromUser(
                this.context,
                ['ohos.permission.ACCELEROMETER']
            );
            return result.permissions[0] === 'ohos.permission.ACCELEROMETER' &&
                result.authResults[0] === 0;
        } catch (err) {
            console.error('権限申請に失敗しました:', err);
            return false;
        }
    }

    public async startDetection(): Promise<void> {
        if (this.isMonitoring) return;
        const hasPermission = await this.requestAccelerationPermission();
        if (!hasPermission) {
            throw new Error('加速度センサーの権限が与えられていません');
        }
        this.isMonitoring = true;
        this.setupAccelerometer();
    }

    private setupAccelerometer(): void {
        try {
            sensor.on(sensor.SensorId.ACCELEROMETER, this.accelerometerCallback);
            console.log('加速度センサーの起動に成功しました');
        } catch (error) {
            console.error('加速度センサーの初期化に失敗しました:', (error as BusinessError).message);
        }
    }

    public stopDetection(): void {
        if (!this.isMonitoring) return;
        this.isMonitoring = false;
        sensor.off(sensor.SensorId.ACCELEROMETER, this.accelerometerCallback);
    }

    private accelerationData: Accelerometer = { x: 0, y: 0, z: 0 };

    getCurrentAcceleration(): Accelerometer {
        return this.accelerationData;
    }

    calculateStride(timeDiff: number): number {
        const accel = this.accelerationData;
        const magnitude = Math.sqrt(accel.x ** 2 + accel.y ** 2 + accel.z ** 2);
        const userProfile = UserProfile.getInstance();

        if (Math.abs(magnitude - 9.8) < 0.5) { // 重力加速度に近い場合は停止と見なします
            return 0;
        }

        const baseStride = userProfile.getHeight() * 0.0045; // メートルに変換
        const dynamicFactor = Math.min(1.5, Math.max(0.8, (magnitude / 9.8) * (70 / userProfile.getWeight())));
        return baseStride * dynamicFactor * timeDiff;
    }
}

核心点解析

• 権限申請:加速度センサーを使用する前に、ohos.permission.ACCELEROMETER権限を申請する必要があります。abilityAccessCtrl.createAtManagerメソッドを使用して権限を申請し、ユーザーが許可したかどうかを確認します。

• データのリスニング:sensor.onメソッドを使用して加速度センサーのデータをリスニングし、リアルタイムでaccelerationDataを更新します。

• 歩幅の計算:ユーザーの身長と加速度データを組み合わせて、動的に歩幅を計算します。停止状態では0歩幅を返し、誤判定を避けることができます。

二、室内運動データの推定

室内運動の場面では、GPS測位に頼ることができないため、歩数と歩幅を使用して運動距離と速度を推定する必要があります。以下がコア計算ロジックです。

addPointBySteps(): number {
    const currentSteps = this.stepCounterService?.getCurrentSteps() ?? 0;
    const userProfile = UserProfile.getInstance();
    const accelerationService = AccelerationSensorService.getInstance(this.context);

    const point = new RunPoint(0, 0);
    const currentTime = Date.now();
    point.netDuration = Math.floor((currentTime - this.startTime) / 1000);
    point.totalDuration = point.netDuration + Math.floor(this.totalDuration);

    const pressureService = PressureDetectionService.getInstance();
    point.altitude = pressureService.getCurrentAltitude();
    point.totalAscent = pressureService.getTotalAscent();
    point.totalDescent = pressureService.getTotalDescent();
    point.steps = currentSteps;

    if (this.runState === RunState.Running) {
        const stepDiff = currentSteps - (this.previousPoint?.steps ?? 0);
        const timeDiff = (currentTime - (this.previousPoint?.timestamp ?? currentTime)) / 1000;

        const accelData = accelerationService.getCurrentAcceleration();
        const magnitude = Math.sqrt(accelData.x ** 2 + accelData.y ** 2 + accelData.z ** 2);

        let stride = accelerationService.calculateStride(timeDiff);
        if (stepDiff > 0 && stride > 0) {
            const distanceBySteps = stepDiff * stride;
            this.totalDistance += distanceBySteps / 1000;

            point.netDistance = this.totalDistance * 1000;
            point.totalDistance = point.netDistance;

            console.log(`歩数の変化: ${stepDiff}, 歩幅: ${stride.toFixed(2)}m, 距離の増加: ${distanceBySteps.toFixed(2)}m`);
        }

        if (this.previousPoint && timeDiff > 0) {
            const instantCadence = stepDiff > 0 ? (stepDiff / timeDiff) * 60 : 0;
            point.cadence = this.currentPoint ?
                (this.currentPoint.cadence * 0.7 + instantCadence * 0.3) :
                instantCadence;

            const instantSpeed = distanceBySteps / timeDiff;
            point.speed = this.currentPoint ?
                (this.currentPoint.speed * 0.7 + instantSpeed * 0.3) :
                instantSpeed;

            point.stride = stride;
        } else {
            point.cadence = this.currentPoint?.cadence ?? 0;
            point.speed = this.currentPoint?.speed ?? 0;
            point.stride = stride;
        }

        if (this.exerciseType && userProfile && this.previousPoint) {
            const distance = point.netDuration;
            const ascent = point.totalAscent - this.previousPoint.totalAscent;
            const descent = point.totalDescent - this.previousPoint.totalDescent;
            const newCalories = CalorieCalculator.calculateCalories(
                this.exerciseType,
                userProfile.getWeight(),
                userProfile.getAge(),
                userProfile.getGender(),
                0, // 心拍数データはまだ使用しません
                ascent,
                descent,
                distance
            );
            point.calories = this.previousPoint.calories + newCalories;
        }
    }

    this.previousPoint = this.currentPoint;
    this.currentPoint = point;

    if (this.currentSport && this.runState === RunState.Running) {
        this.currentSport.distance = this.totalDistance * 1000;
        this.currentSport.calories = point.calories;
        this.sportDataService.saveCurrentSport(this.currentSport);
    }

    return this.totalDistance;
}

核心点解析

• 歩数の差と時間の差:現在の歩数と最後に記録された歩数の差を時間の差と組み合わせることで、歩頻と歩幅を計算します。

• 歩幅の動的調整:加速度データを使用して歩幅を動的に調整し、異なる運動強度における正確さを保証します。

• 速度とカロリーの計算:歩幅と歩数の差を組み合わせることで、運動速度と消費されるカロリーを計算します。

• データの平滑化処理:移動平均法を使用して歩頻と速度を平滑化処理し、データの変動を減らします。

三、毎秒データを更新する

リアルタイムで運動データを表示するためには、データを毎秒更新する必要があります。以下がタイマーの実装ロジックです。

 private startTimer(): void {
    if (this.timerInterval === null) {
      this.timerInterval = setInterval(() => {
        if (this.runState === RunState.Running) {
          this.netDuration = Math.floor((Date.now() - this.startTime) / 1000);
          // 室内走: 歩数を使用してトラックポイントを追加する
          if (this.exerciseType?.sportType === SportType.INDOOR) {
            this.addPointBySteps(); // 新しい呼び出し
          }
          // 現在のペース(秒/キロメートル)を計算する
          let currentPace = 0;
          if (this.totalDistance > 0) {
            currentPace = Math.floor(this.netDuration / this.totalDistance);
          }
          if (this.currentPoint) {
            this.currentPoint.pace = currentPace;
          }
          // すべてのリスナーに通知する
          this.timeListeners.forEach(listener => {
            listener.onTimeUpdate(this.netDuration, this.currentPoint);
          });
        }
      }, 1000); // 毎秒1回更新する
    }
  }

核心点解析

  1. タイマーの設定setIntervalメソッドを使用して、1秒ごとにデータ更新ロジックをトリガーします。
  2. 運動状態の判断:運動状態がRunningの場合のみ、データを更新します。
  3. ペースの計算:総時間と総距離の比率を使用して現在のペースを計算します。
  4. リスナーへの通知:更新されたデータをリスナーに渡し、データのリアルタイム表示を保証します。

四、最適化と改善

1. データの平滑化処理

実際の運動プロセスでは、加速度データはさまざまな要因によって干渉を受ける可能性があるため、データの変動が大きくなることがあります。データの正確性和安定性を高めるために、移動平均法を使用して歩頻と速度を平滑化処理しました。

point.cadence = this.currentPoint ?
    (this.currentPoint.cadence * 0.7 + instantCadence * 0.3) :
    instantCadence;

point.speed = this.currentPoint ?
    (this.currentPoint.speed * 0.7 + instantSpeed * 0.3) :
    instantSpeed;

この方法を使用することで、データの短期的な変動を効果的に減らし、運動データをより平滑で安定したものにすることができます。

2. 歩幅の動的調整

歩幅は、ユーザーの運動強度と身体状態によって変化します。歩幅をより正確に推定するために、動的調整メカニズムを導入しました。

let stride = accelerationService.calculateStride(timeDiff);

calculateStrideメソッドでは、ユーザーの身長、体重、加速度データを組み合わせて、動的に歩幅を計算します。この方法は、異なるユーザーの運動状態に更好地適応することができます。

五、まとめと展望

加速度センサーとタイマーを使用して、室内運動の距離、速度、歩幅を成功裏に推定しました。これらの機能は、ユーザーが自分の運動状態を更好地理解するだけでなく、運動健康管理に重要なデータを提供することができます。

Discussion