找回密码
 立即注册
首页 业界区 业界 HarmonyOS运动开发:精准估算室内运动的距离、速度与步 ...

HarmonyOS运动开发:精准估算室内运动的距离、速度与步幅

郦惠 3 天前
前言
在室内运动场景中,由于缺乏 GPS 信号,传统的基于卫星定位的运动数据追踪方法无法使用。因此,如何准确估算室内运动的距离、速度和步幅,成为了运动应用开发中的一个重要挑战。本文将结合鸿蒙(HarmonyOS)开发实战经验,深入解析如何利用加速度传感器等设备功能,实现室内运动数据的精准估算。
一、加速度传感器:室内运动数据的核心
加速度传感器是实现室内运动数据估算的关键硬件。它能够实时监测设备在三个轴向上的加速度变化,从而为运动状态分析提供基础数据。以下是加速度传感器服务类的核心代码:
  1. import common from '@ohos.app.ability.common';
  2. import sensor from '@ohos.sensor';
  3. import { BusinessError } from '@kit.BasicServicesKit';
  4. import { abilityAccessCtrl } from '@kit.AbilityKit';
  5. import { UserProfile } from '../user/UserProfile';
  6. interface Accelerometer {
  7.     x: number;
  8.     y: number;
  9.     z: number;
  10. }
  11. export class AccelerationSensorService {
  12.     private static instance: AccelerationSensorService | null = null;
  13.     private context: common.UIAbilityContext;
  14.     private isMonitoring: boolean = false; // 是否正在监听
  15.     private constructor(context: common.UIAbilityContext) {
  16.         this.context = context;
  17.     }
  18.     static getInstance(context: common.UIAbilityContext): AccelerationSensorService {
  19.         if (!AccelerationSensorService.instance) {
  20.             AccelerationSensorService.instance = new AccelerationSensorService(context);
  21.         }
  22.         return AccelerationSensorService.instance;
  23.     }
  24.     private accelerometerCallback = (data: sensor.AccelerometerResponse) => {
  25.         this.accelerationData = {
  26.             x: data.x,
  27.             y: data.y,
  28.             z: data.z
  29.         };
  30.     };
  31.     private async requestAccelerationPermission(): Promise<boolean> {
  32.         const atManager = abilityAccessCtrl.createAtManager();
  33.         try {
  34.             const result = await atManager.requestPermissionsFromUser(
  35.                 this.context,
  36.                 ['ohos.permission.ACCELEROMETER']
  37.             );
  38.             return result.permissions[0] === 'ohos.permission.ACCELEROMETER' &&
  39.                 result.authResults[0] === 0;
  40.         } catch (err) {
  41.             console.error('申请权限失败:', err);
  42.             return false;
  43.         }
  44.     }
  45.     public async startDetection(): Promise<void> {
  46.         if (this.isMonitoring) return;
  47.         const hasPermission = await this.requestAccelerationPermission();
  48.         if (!hasPermission) {
  49.             throw new Error('未授予加速度传感器权限');
  50.         }
  51.         this.isMonitoring = true;
  52.         this.setupAccelerometer();
  53.     }
  54.     private setupAccelerometer(): void {
  55.         try {
  56.             sensor.on(sensor.SensorId.ACCELEROMETER, this.accelerometerCallback);
  57.             console.log('加速度传感器启动成功');
  58.         } catch (error) {
  59.             console.error('加速度传感器初始化失败:', (error as BusinessError).message);
  60.         }
  61.     }
  62.     public stopDetection(): void {
  63.         if (!this.isMonitoring) return;
  64.         this.isMonitoring = false;
  65.         sensor.off(sensor.SensorId.ACCELEROMETER, this.accelerometerCallback);
  66.     }
  67.     private accelerationData: Accelerometer = { x: 0, y: 0, z: 0 };
  68.     getCurrentAcceleration(): Accelerometer {
  69.         return this.accelerationData;
  70.     }
  71.     calculateStride(timeDiff: number): number {
  72.         const accel = this.accelerationData;
  73.         const magnitude = Math.sqrt(accel.x ** 2 + accel.y ** 2 + accel.z ** 2);
  74.         const userProfile = UserProfile.getInstance();
  75.         if (Math.abs(magnitude - 9.8) < 0.5) { // 接近重力加速度时视为静止
  76.             return 0;
  77.         }
  78.         const baseStride = userProfile.getHeight() * 0.0045; // 转换为米
  79.         const dynamicFactor = Math.min(1.5, Math.max(0.8, (magnitude / 9.8) * (70 / userProfile.getWeight())));
  80.         return baseStride * dynamicFactor * timeDiff;
  81.     }
  82. }
复制代码
核心点解析
• 权限申请:在使用加速度传感器之前,必须申请ohos.permission.ACCELEROMETER权限。通过abilityAccessCtrl.createAtManager方法申请权限,并检查用户是否授权。
• 数据监听:通过sensor.on方法监听加速度传感器数据,实时更新accelerationData。
• 步幅计算:结合用户身高和加速度数据动态计算步幅。静止状态下返回 0 步幅,避免误判。
二、室内运动数据的估算
在室内运动场景中,我们无法依赖 GPS 定位,因此需要通过步数和步幅来估算运动距离和速度。以下是核心计算逻辑:
  1. addPointBySteps(): number {
  2.     const currentSteps = this.stepCounterService?.getCurrentSteps() ?? 0;
  3.     const userProfile = UserProfile.getInstance();
  4.     const accelerationService = AccelerationSensorService.getInstance(this.context);
  5.     const point = new RunPoint(0, 0);
  6.     const currentTime = Date.now();
  7.     point.netDuration = Math.floor((currentTime - this.startTime) / 1000);
  8.     point.totalDuration = point.netDuration + Math.floor(this.totalDuration);
  9.     const pressureService = PressureDetectionService.getInstance();
  10.     point.altitude = pressureService.getCurrentAltitude();
  11.     point.totalAscent = pressureService.getTotalAscent();
  12.     point.totalDescent = pressureService.getTotalDescent();
  13.     point.steps = currentSteps;
  14.     if (this.runState === RunState.Running) {
  15.         const stepDiff = currentSteps - (this.previousPoint?.steps ?? 0);
  16.         const timeDiff = (currentTime - (this.previousPoint?.timestamp ?? currentTime)) / 1000;
  17.         const accelData = accelerationService.getCurrentAcceleration();
  18.         const magnitude = Math.sqrt(accelData.x ** 2 + accelData.y ** 2 + accelData.z ** 2);
  19.         let stride = accelerationService.calculateStride(timeDiff);
  20.         if (stepDiff > 0 && stride > 0) {
  21.             const distanceBySteps = stepDiff * stride;
  22.             this.totalDistance += distanceBySteps / 1000;
  23.             point.netDistance = this.totalDistance * 1000;
  24.             point.totalDistance = point.netDistance;
  25.             console.log(`步数变化: ${stepDiff}, 步幅: ${stride.toFixed(2)}m, 距离增量: ${distanceBySteps.toFixed(2)}m`);
  26.         }
  27.         if (this.previousPoint && timeDiff > 0) {
  28.             const instantCadence = stepDiff > 0 ? (stepDiff / timeDiff) * 60 : 0;
  29.             point.cadence = this.currentPoint ?
  30.                 (this.currentPoint.cadence * 0.7 + instantCadence * 0.3) :
  31.                 instantCadence;
  32.             const instantSpeed = distanceBySteps / timeDiff;
  33.             point.speed = this.currentPoint ?
  34.                 (this.currentPoint.speed * 0.7 + instantSpeed * 0.3) :
  35.                 instantSpeed;
  36.             point.stride = stride;
  37.         } else {
  38.             point.cadence = this.currentPoint?.cadence ?? 0;
  39.             point.speed = this.currentPoint?.speed ?? 0;
  40.             point.stride = stride;
  41.         }
  42.         if (this.exerciseType && userProfile && this.previousPoint) {
  43.             const distance = point.netDuration;
  44.             const ascent = point.totalAscent - this.previousPoint.totalAscent;
  45.             const descent = point.totalDescent - this.previousPoint.totalDescent;
  46.             const newCalories = CalorieCalculator.calculateCalories(
  47.                 this.exerciseType,
  48.                 userProfile.getWeight(),
  49.                 userProfile.getAge(),
  50.                 userProfile.getGender(),
  51.                 0, // 暂不使用心率数据
  52.                 ascent,
  53.                 descent,
  54.                 distance
  55.             );
  56.             point.calories = this.previousPoint.calories + newCalories;
  57.         }
  58.     }
  59.     this.previousPoint = this.currentPoint;
  60.     this.currentPoint = point;
  61.     if (this.currentSport && this.runState === RunState.Running) {
  62.         this.currentSport.distance = this.totalDistance * 1000;
  63.         this.currentSport.calories = point.calories;
  64.         this.sportDataService.saveCurrentSport(this.currentSport);
  65.     }
  66.     return this.totalDistance;
  67. }
复制代码
核心点解析
• 步数差与时间差:通过当前步数与上一次记录的步数差值,结合时间差,计算出步频和步幅。
• 动态步幅调整:根据加速度数据动态调整步幅,确保在不同运动强度下的准确性。
• 速度与卡路里计算:结合步幅和步数差值,计算出运动速度和消耗的卡路里。
• 数据平滑处理:使用移动平均法对步频和速度进行平滑处理,减少数据波动。
三、每秒更新数据
为了实时展示运动数据,我们需要每秒更新一次数据。以下是定时器的实现逻辑:
  1. private startTimer(): void {
  2.     if (this.timerInterval === null) {
  3.       this.timerInterval = setInterval(() => {
  4.         if (this.runState === RunState.Running) {
  5.           this.netDuration = Math.floor((Date.now() - this.startTime) / 1000);
  6.           // 室内跑:使用步数添加轨迹点
  7.           if (this.exerciseType?.sportType === SportType.INDOOR) {
  8.             this.addPointBySteps(); // 新增调用
  9.           }
  10.           // 计算当前配速(秒/公里)
  11.           let currentPace = 0;
  12.           if (this.totalDistance > 0) {
  13.             currentPace = Math.floor(this.netDuration / this.totalDistance);
  14.           }
  15.           if (this.currentPoint) {
  16.             this.currentPoint.pace = currentPace;
  17.           }
  18.           // 通知所有监听器
  19.           this.timeListeners.forEach(listener => {
  20.             listener.onTimeUpdate(this.netDuration, this.currentPoint);
  21.           });
  22.         }
  23.       }, 1000); // 每1秒更新一次
  24.     }
  25.   }
复制代码
核心点解析


  • 定时器设置:使用 setInterval 方法每秒触发一次数据更新逻辑。
  • 运动状态判断:只有在运动状态为 Running 时,才进行数据更新。
  • 配速计算:通过总时间与总距离的比值计算当前配速。
  • 通知监听器:将更新后的数据通过监听器传递给其他组件,确保数据的实时展示。
四、优化与改进

1. 数据平滑处理

在实际运动过程中,加速度数据可能会受到多种因素的干扰,导致数据波动较大。为了提高数据的准确性和稳定性,我们采用了移动平均法对步频和速度进行平滑处理:
  1. point.cadence = this.currentPoint ?
  2.     (this.currentPoint.cadence * 0.7 + instantCadence * 0.3) :
  3.     instantCadence;
  4. point.speed = this.currentPoint ?
  5.     (this.currentPoint.speed * 0.7 + instantSpeed * 0.3) :
  6.     instantSpeed;
复制代码
通过这种方式,可以有效减少数据的短期波动,使运动数据更加平滑和稳定。
2.动态步幅调整
步幅会因用户的运动强度和身体状态而变化。为了更准确地估算步幅,我们引入了动态调整机制:
  1. let stride = accelerationService.calculateStride(timeDiff);
复制代码
在calculateStride方法中,结合用户的身高、体重和加速度数据,动态计算步幅。这种方法可以更好地适应不同用户的运动状态。
五、总结与展望
通过加速度传感器和定时器,我们成功实现了室内运动的距离、速度和步幅估算。这些功能不仅能够帮助用户更好地了解自己的运动状态,还能为运动健康管理提供重要数据支持。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册