DEV Community

zhonghua
zhonghua

Posted on • Edited on

Practical HarmonyOS Sports Development: Accurate Estimation of Indoor Sports Distance, Speed, and Stride Length

Practical HarmonyOS Sports Development: Accurate Estimation of Indoor Sports Distance, Speed, and Stride Length

Foreword

In indoor sports scenarios, the lack of GPS signals makes traditional satellite-based sports data tracking methods unusable. Therefore, how to accurately estimate the distance, speed, and stride length of indoor sports has become an important challenge in sports application development. This article will combine practical HarmonyOS development experience to deeply analyze how to use the accelerometer and other device functions to achieve accurate estimation of indoor sports data.

I. Accelerometer: The Core of Indoor Sports Data

The accelerometer is the key hardware for estimating indoor sports data. It can monitor the acceleration changes of the device in three axes in real time, providing basic data for sports status analysis. Below is the core code of the accelerometer service class:

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; // Whether it is currently listening

    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('Permission request failed:', err);
            return false;
        }
    }

    public async startDetection(): Promise<void> {
        if (this.isMonitoring) return;
        const hasPermission = await this.requestAccelerationPermission();
        if (!hasPermission) {
            throw new Error('Accelerometer permission not granted');
        }
        this.isMonitoring = true;
        this.setupAccelerometer();
    }

    private setupAccelerometer(): void {
        try {
            sensor.on(sensor.SensorId.ACCELEROMETER, this.accelerometerCallback);
            console.log('Accelerometer started successfully');
        } catch (error) {
            console.error('Accelerometer initialization failed:', (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) { // Consider it as stationary when close to gravitational acceleration
            return 0;
        }

        const baseStride = userProfile.getHeight() * 0.0045; // Convert to meters
        const dynamicFactor = Math.min(1.5, Math.max(0.8, (magnitude / 9.8) * (70 / userProfile.getWeight())));
        return baseStride * dynamicFactor * timeDiff;
    }
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis

  • Permission Request: Before using the accelerometer, you must request the ohos.permission.ACCELEROMETER permission. Use the abilityAccessCtrl.createAtManager method to request permission and check if the user has authorized it.

  • Data Monitoring: Use the sensor.on method to monitor accelerometer data and update accelerationData in real time.

  • Stride Calculation: Dynamically calculate stride length based on user height and acceleration data. Return 0 stride length when stationary to avoid misjudgment.

II. Estimation of Indoor Sports Data

In indoor sports scenarios, we cannot rely on GPS positioning, so we need to estimate the distance and speed through the number of steps and stride length. Below is the core calculation logic:

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(`Step difference: ${stepDiff}, Stride: ${stride.toFixed(2)}m, Distance increment: ${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, // Heart rate data not used for now
                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;
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis

  • Step Difference and Time Difference: Calculate cadence and stride length by combining the difference in steps between the current step count and the last recorded step count with the time difference.

  • Dynamic Stride Adjustment: Dynamically adjust stride length based on acceleration data to ensure accuracy under different exercise intensities.

  • Speed and Calorie Calculation: Combine stride length and step difference to calculate exercise speed and calories burned.

  • Data Smoothing: Use the moving average method to smooth cadence and speed, reducing data fluctuations.

III. Updating Data Every Second

To display sports data in real time, we need to update the data once every second. Below is the logic for the timer implementation:

private startTimer(): void {
    if (this.timerInterval === null) {
        this.timerInterval = setInterval(() => {
            if (this.runState === RunState.Running) {
                this.netDuration = Math.floor((Date.now() - this.startTime) / 1000);
                // Indoor running: Use steps to add trajectory points
                if (this.exerciseType?.sportType === SportType.INDOOR) {
                    this.addPointBySteps(); // New call
                }
                // Calculate current pace (seconds per kilometer)
                let currentPace = 0;
                if (this.totalDistance > 0) {
                    currentPace = Math.floor(this.netDuration / this.totalDistance);
                }
                if (this.currentPoint) {
                    this.currentPoint.pace = currentPace;
                }
                // Notify all listeners
                this.timeListeners.forEach(listener => {
                    listener.onTimeUpdate(this.netDuration, this.currentPoint);
                });
            }
        }, 1000); // Update every 1 second
    }
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis

  1. Timer Setup: Use the setInterval method to trigger data update logic once per second.
  2. Exercise State Check: Only update data when the exercise state is Running.
  3. Pace Calculation: Calculate the current pace by the ratio of total time to total distance.
  4. Notify Listeners: Pass the updated data through listeners to other components to ensure real-time data display.

IV. Optimization and Improvement

  1. Data Smoothing

In actual sports, acceleration data may be disturbed by various factors, leading to significant data fluctuations. To improve the accuracy and stability of the data, we use the moving average method to smooth cadence and speed:

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;
Enter fullscreen mode Exit fullscreen mode

By doing so, we can effectively reduce short-term data fluctuations and make sports data smoother and more stable.

  1. Dynamic Stride Adjustment

Stride length can vary depending on the user's exercise intensity and physical condition. To more accurately estimate stride length, we introduce a dynamic adjustment mechanism:

let stride = accelerationService.calculateStride(timeDiff);
Enter fullscreen mode Exit fullscreen mode

In the calculateStride method, we dynamically calculate stride length based on the user's height, weight, and acceleration data. This method can better adapt to the exercise state of different users.

V. Summary and Outlook

Through the accelerometer and timer, we have successfully estimated the distance, speed, and stride length of indoor sports. These functions not only help users better understand their sports status but also provide important data support for sports health management.

Top comments (0)