HarmonyOS運(yùn)動開發(fā):精準(zhǔn)估算室內(nèi)運(yùn)動的距離、速度與步幅

前言

在室內(nèi)運(yùn)動場景中,由于缺乏 GPS 信號,傳統(tǒng)的基于衛(wèi)星定位的運(yùn)動數(shù)據(jù)追蹤方法無法使用。因此,如何準(zhǔn)確估算室內(nèi)運(yùn)動的距離、速度和步幅,成為了運(yùn)動應(yīng)用開發(fā)中的一個重要挑戰(zhàn)。本文將結(jié)合鴻蒙(HarmonyOS)開發(fā)實(shí)戰(zhàn)經(jīng)驗(yàn),深入解析如何利用加速度傳感器等設(shè)備功能,實(shí)現(xiàn)室內(nèi)運(yùn)動數(shù)據(jù)的精準(zhǔn)估算。

一、加速度傳感器:室內(nèi)運(yùn)動數(shù)據(jù)的核心

加速度傳感器是實(shí)現(xiàn)室內(nèi)運(yùn)動數(shù)據(jù)估算的關(guān)鍵硬件。它能夠?qū)崟r監(jiān)測設(shè)備在三個軸向上的加速度變化,從而為運(yùn)動狀態(tài)分析提供基礎(chǔ)數(shù)據(jù)。以下是加速度傳感器服務(wù)類的核心代碼:

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; // 是否正在監(jiān)聽

    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('申請權(quán)限失敗:', err);
            return false;
        }
    }

    public async startDetection(): Promise<void> {
        if (this.isMonitoring) return;
        const hasPermission = await this.requestAccelerationPermission();
        if (!hasPermission) {
            throw new Error('未授予加速度傳感器權(quán)限');
        }
        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; // 轉(zhuǎn)換為米
        const dynamicFactor = Math.min(1.5, Math.max(0.8, (magnitude / 9.8) * (70 / userProfile.getWeight())));
        return baseStride * dynamicFactor * timeDiff;
    }
}

核心點(diǎn)解析

? 權(quán)限申請:在使用加速度傳感器之前,必須申請ohos.permission.ACCELEROMETER權(quán)限。通過abilityAccessCtrl.createAtManager方法申請權(quán)限,并檢查用戶是否授權(quán)。

? 數(shù)據(jù)監(jiān)聽:通過sensor.on方法監(jiān)聽加速度傳感器數(shù)據(jù),實(shí)時更新accelerationData。

? 步幅計(jì)算:結(jié)合用戶身高和加速度數(shù)據(jù)動態(tài)計(jì)算步幅。靜止?fàn)顟B(tài)下返回 0 步幅,避免誤判。

二、室內(nèi)運(yùn)動數(shù)據(jù)的估算

在室內(nèi)運(yùn)動場景中,我們無法依賴 GPS 定位,因此需要通過步數(shù)和步幅來估算運(yùn)動距離和速度。以下是核心計(jì)算邏輯:

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(`步數(shù)變化: ${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, // 暫不使用心率數(shù)據(jù)
                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;
}

核心點(diǎn)解析

? 步數(shù)差與時間差:通過當(dāng)前步數(shù)與上一次記錄的步數(shù)差值,結(jié)合時間差,計(jì)算出步頻和步幅。

? 動態(tài)步幅調(diào)整:根據(jù)加速度數(shù)據(jù)動態(tài)調(diào)整步幅,確保在不同運(yùn)動強(qiáng)度下的準(zhǔn)確性。

? 速度與卡路里計(jì)算:結(jié)合步幅和步數(shù)差值,計(jì)算出運(yùn)動速度和消耗的卡路里。

? 數(shù)據(jù)平滑處理:使用移動平均法對步頻和速度進(jìn)行平滑處理,減少數(shù)據(jù)波動。

三、每秒更新數(shù)據(jù)

為了實(shí)時展示運(yùn)動數(shù)據(jù),我們需要每秒更新一次數(shù)據(jù)。以下是定時器的實(shí)現(xiàn)邏輯:

 private startTimer(): void {
    if (this.timerInterval === null) {
      this.timerInterval = setInterval(() => {
        if (this.runState === RunState.Running) {
          this.netDuration = Math.floor((Date.now() - this.startTime) / 1000);
          // 室內(nèi)跑:使用步數(shù)添加軌跡點(diǎn)
          if (this.exerciseType?.sportType === SportType.INDOOR) {
            this.addPointBySteps(); // 新增調(diào)用
          }
          // 計(jì)算當(dāng)前配速(秒/公里)
          let currentPace = 0;
          if (this.totalDistance > 0) {
            currentPace = Math.floor(this.netDuration / this.totalDistance);
          }
          if (this.currentPoint) {
            this.currentPoint.pace = currentPace;
          }
          // 通知所有監(jiān)聽器
          this.timeListeners.forEach(listener => {
            listener.onTimeUpdate(this.netDuration, this.currentPoint);
          });
        }
      }, 1000); // 每1秒更新一次
    }
  }

核心點(diǎn)解析

  1. 定時器設(shè)置:使用 setInterval 方法每秒觸發(fā)一次數(shù)據(jù)更新邏輯。
  2. 運(yùn)動狀態(tài)判斷:只有在運(yùn)動狀態(tài)為 Running 時,才進(jìn)行數(shù)據(jù)更新。
  3. 配速計(jì)算:通過總時間與總距離的比值計(jì)算當(dāng)前配速。
  4. 通知監(jiān)聽器:將更新后的數(shù)據(jù)通過監(jiān)聽器傳遞給其他組件,確保數(shù)據(jù)的實(shí)時展示。

四、優(yōu)化與改進(jìn)

1. 數(shù)據(jù)平滑處理

在實(shí)際運(yùn)動過程中,加速度數(shù)據(jù)可能會受到多種因素的干擾,導(dǎo)致數(shù)據(jù)波動較大。為了提高數(shù)據(jù)的準(zhǔn)確性和穩(wěn)定性,我們采用了移動平均法對步頻和速度進(jìn)行平滑處理:

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;

通過這種方式,可以有效減少數(shù)據(jù)的短期波動,使運(yùn)動數(shù)據(jù)更加平滑和穩(wěn)定。

2.動態(tài)步幅調(diào)整

步幅會因用戶的運(yùn)動強(qiáng)度和身體狀態(tài)而變化。為了更準(zhǔn)確地估算步幅,我們引入了動態(tài)調(diào)整機(jī)制:

let stride = accelerationService.calculateStride(timeDiff);

calculateStride方法中,結(jié)合用戶的身高、體重和加速度數(shù)據(jù),動態(tài)計(jì)算步幅。這種方法可以更好地適應(yīng)不同用戶的運(yùn)動狀態(tài)。

五、總結(jié)與展望

通過加速度傳感器和定時器,我們成功實(shí)現(xiàn)了室內(nèi)運(yùn)動的距離、速度和步幅估算。這些功能不僅能夠幫助用戶更好地了解自己的運(yùn)動狀態(tài),還能為運(yùn)動健康管理提供重要數(shù)據(jù)支持。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容