##鴻蒙核心技術##運動開發##Sensor Service Kit(傳感器服務)#
前言
在室內運動場景中,由于缺乏 GPS 信號,傳統的基于衛星定位的運動數據追蹤方法無法使用。因此,如何準確估算室內運動的距離、速度和步幅,成為了運動應用開發中的一個重要挑戰。本文將結合鴻蒙(HarmonyOS)開發實戰經驗,深入解析如何利用加速度傳感器等設備功能,實現室內運動數據的精準估算。
一、加速度傳感器:室內運動數據的核心
加速度傳感器是實現室內運動數據估算的關鍵硬件。它能夠實時監測設備在三個軸向上的加速度變化,從而為運動狀態分析提供基礎數據。以下是加速度傳感器服務類的核心代碼:
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秒更新一次}}
核心點解析
- 定時器設置:使用
setInterval
方法每秒觸發一次數據更新邏輯。 - 運動狀態判斷:只有在運動狀態為
Running
時,才進行數據更新。 - 配速計算:通過總時間與總距離的比值計算當前配速。
- 通知監聽器:將更新后的數據通過監聽器傳遞給其他組件,確保數據的實時展示。
四、優化與改進
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
方法中,結合用戶的身高、體重和加速度數據,動態計算步幅。這種方法可以更好地適應不同用戶的運動狀態。
五、總結與展望
通過加速度傳感器和定時器,我們成功實現了室內運動的距離、速度和步幅估算。這些功能不僅能夠幫助用戶更好地了解自己的運動狀態,還能為運動健康管理提供重要數據支持。