鸿蒙App开发--雪痕App怎么做传感器融合?坡度计算详解
滑雪App怎么做传感器融合?坡度计算详解
上一篇我们聊了雪痕的GPS轨迹记录,这篇来聊点更高级的——传感器融合与坡度计算。如果你还没体验过雪痕,可以去鸿蒙应用市场搜一下**「雪痕」**,下载下来滑一趟,看看坡度数据是怎么显示的。体验完了再回来看这篇文章,你会更清楚传感器融合的算法原理。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。
上一篇我们解决了"怎么记录GPS轨迹"的问题,这篇来解决"怎么计算坡度"的问题。
这个需求在Web端几乎不可能实现,因为你很难在浏览器里拿到高频率的传感器数据。鸿蒙端的好处是,加速度计和陀螺仪可以做到毫秒级采样,数据足够精细,可以用来做姿态估计。
这篇文章聊什么
雪痕的传感器融合功能,核心要解决的问题是:
- 数据怎么采集 — 用加速度计和陀螺仪采集姿态数据
- 坡度怎么计算 — 根据重力分量计算倾斜角度
- 数据怎么融合 — 用互补滤波融合两个传感器的数据
第一步:理解传感器
滑雪App需要两个传感器配合工作:
- 加速度计(SENSOR_TYPE_ID_ACCELEROMETER)— 测量加速度,包括重力
- 陀螺仪(SENSOR_TYPE_ID_GYROSCOPE)— 测量角速度
为什么需要两个传感器?
- 加速度计:可以测量重力方向,从而计算坡度。但受运动干扰大。
- 陀螺仪:可以测量旋转速度,积分得到角度。但有漂移。
两者互补:加速度计提供长期稳定的参考,陀螺仪提供短期精确的变化。
第二步:权限申请
使用传感器之前,需要在module.json5里声明权限:
{
"requestPermissions": [
{
"name": "ohos.permission.ACTIVITY_MOTION",
"reason": "用于加速度计和陀螺仪获取运动数据",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
]
}
第三步:封装传感器服务
同时订阅加速度计和陀螺仪:
// SensorService.ets
import { sensor } from '@kit.SensorServiceKit';
export class SensorService {
private accelCallback: ((data: AccelData) => void) | null = null;
private gyroCallback: ((data: GyroData) => void) | null = null;
startAccel(callback: (data: AccelData) => void) {
this.accelCallback = callback;
sensor.on(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
this.accelCallback?.({
x: data.x,
y: data.y,
z: data.z,
timestamp: Date.now()
});
}, { interval: 20000000 }); // 20ms (50Hz)
}
startGyro(callback: (data: GyroData) => void) {
this.gyroCallback = callback;
sensor.on(sensor.SensorId.GYROSCOPE, (data: sensor.GyroscopeResponse) => {
this.gyroCallback?.({
x: data.x,
y: data.y,
z: data.z,
timestamp: Date.now()
});
}, { interval: 20000000 }); // 20ms (50Hz)
}
stopAccel() {
sensor.off(sensor.SensorId.ACCELEROMETER);
this.accelCallback = null;
}
stopGyro() {
sensor.off(sensor.SensorId.GYROSCOPE);
this.gyroCallback = null;
}
stopAll() {
this.stopAccel();
this.stopGyro();
}
}
interface AccelData {
x: number;
y: number;
z: number;
timestamp: number;
}
interface GyroData {
x: number;
y: number;
z: number;
timestamp: number;
}
React对应版本(模拟数据):
// React - 模拟传感器服务
function useSensors() {
const [accel, setAccel] = useState({ x: 0, y: 0, z: 9.8 });
const [gyro, setGyro] = useState({ x: 0, y: 0, z: 0 });
useEffect(() => {
const interval = setInterval(() => {
setAccel({
x: (Math.random() - 0.5) * 2,
y: -9.8 + (Math.random() - 0.5) * 1, // 重力
z: (Math.random() - 0.5) * 2,
timestamp: Date.now()
});
setGyro({
x: (Math.random() - 0.5) * 0.5,
y: (Math.random() - 0.5) * 0.5,
z: (Math.random() - 0.5) * 0.5,
timestamp: Date.now()
});
}, 20);
return () => clearInterval(interval);
}, []);
return { accel, gyro };
}
第四步:计算坡度
根据加速度计数据计算坡度。核心思路是:测量重力在手机坐标系中的分量。
// SlopeCalculator.ets
export class SlopeCalculator {
// 从加速度计计算坡度(度)
static calculateFromAccel(accel: AccelData): number {
// 假设手机平放,Y轴朝前
// 坡度 = atan2(垂直分量, 水平分量)
const vertical = accel.y; // 垂直方向的加速度
const horizontal = Math.sqrt(accel.x * accel.x + accel.z * accel.z); // 水平方向的加速度
if (horizontal === 0) return 0;
const angleRad = Math.atan2(vertical, horizontal);
const angleDeg = angleRad * (180 / Math.PI);
return angleDeg;
}
// 从GPS海拔数据计算坡度(度)
static calculateFromAltitude(
alt1: number, dist1: number,
alt2: number, dist2: number
): number {
const altDiff = alt2 - alt1;
const distDiff = dist2 - dist1;
if (distDiff === 0) return 0;
const angleRad = Math.atan2(altDiff, distDiff);
const angleDeg = angleRad * (180 / Math.PI);
return angleDeg;
}
// 坡度百分比
static toPercentage(angleDeg: number): number {
return Math.tan(angleDeg * Math.PI / 180) * 100;
}
// 坡度等级
static getSlopeGrade(angleDeg: number): string {
const absAngle = Math.abs(angleDeg);
if (absAngle < 5) return '平缓';
if (absAngle < 15) return '缓坡';
if (absAngle < 25) return '中坡';
if (absAngle < 35) return '陡坡';
return '极陡';
}
}
React对应版本:
// React - 坡度计算
const SlopeCalculator = {
calculateFromAccel: (accel) => {
const vertical = accel.y;
const horizontal = Math.sqrt(accel.x * accel.x + accel.z * accel.z);
if (horizontal === 0) return 0;
const angleRad = Math.atan2(vertical, horizontal);
return angleRad * (180 / Math.PI);
},
calculateFromAltitude: (alt1, dist1, alt2, dist2) => {
const altDiff = alt2 - alt1;
const distDiff = dist2 - dist1;
if (distDiff === 0) return 0;
const angleRad = Math.atan2(altDiff, distDiff);
return angleRad * (180 / Math.PI);
},
toPercentage: (angleDeg) => {
return Math.tan(angleDeg * Math.PI / 180) * 100;
},
getSlopeGrade: (angleDeg) => {
const absAngle = Math.abs(angleDeg);
if (absAngle < 5) return '平缓';
if (absAngle < 15) return '缓坡';
if (absAngle < 25) return '中坡';
if (absAngle < 35) return '陡坡';
return '极陡';
}
};
第五步:互补滤波
融合加速度计和陀螺仪的数据,用互补滤波:
// ComplementaryFilter.ets
export class ComplementaryFilter {
private angle: number = 0;
private alpha: number = 0.98; // 陀螺仪权重
update(accelAngle: number, gyroRate: number, dt: number): number {
// 互补滤波公式:
// angle = alpha * (angle + gyroRate * dt) + (1 - alpha) * accelAngle
// alpha 越大,越信任陀螺仪(短期精确,长期漂移)
// alpha 越小,越信任加速度计(长期稳定,短期噪声大)
const gyroAngle = this.angle + gyroRate * dt;
this.angle = this.alpha * gyroAngle + (1 - this.alpha) * accelAngle;
return this.angle;
}
reset() {
this.angle = 0;
}
}
React对应版本:
// React - 互补滤波
function useComplementaryFilter() {
const angleRef = useRef(0);
const alpha = 0.98;
const update = useCallback((accelAngle, gyroRate, dt) => {
const gyroAngle = angleRef.current + gyroRate * dt;
angleRef.current = alpha * gyroAngle + (1 - alpha) * accelAngle;
return angleRef.current;
}, []);
const reset = useCallback(() => {
angleRef.current = 0;
}, []);
return { update, reset };
}
第六步:在滑雪页面集成
把传感器融合集成到滑雪页面:
// ArkTS - 滑雪页面集成传感器
@Component
struct SkiActive {
@State slope: number = 0;
@State slopeGrade: string = '平缓';
@State speed: number = 0;
private sensorService: SensorService = new SensorService();
private slopeFilter: ComplementaryFilter = new ComplementaryFilter();
private lastTimestamp: number = 0;
startSki() {
this.sensorService.startAccel((accel) => {
const accelAngle = SlopeCalculator.calculateFromAccel(accel);
const now = accel.timestamp;
const dt = this.lastTimestamp > 0 ? (now - this.lastTimestamp) / 1000 : 0.02;
this.lastTimestamp = now;
this.slope = this.slopeFilter.update(accelAngle, 0, dt);
this.slopeGrade = SlopeCalculator.getSlopeGrade(this.slope);
});
this.sensorService.startGyro((gyro) => {
// 陀螺仪数据可以用于更精确的坡度计算
});
}
stopSki() {
this.sensorService.stopAll();
}
build() {
Column() {
Text(`${this.slope.toFixed(1)}°`)
.fontSize(48)
.fontWeight(FontWeight.Bold)
Text(this.slopeGrade)
.fontSize(16)
.fontColor('#666')
Text(`${SlopeCalculator.toPercentage(this.slope).toFixed(0)}%`)
.fontSize(24)
.fontColor('#f59e0b')
}
}
}
React对应版本:
// React - 滑雪页面集成传感器
function SkiActive() {
const [slope, setSlope] = useState(0);
const [slopeGrade, setSlopeGrade] = useState('平缓');
const { accel, gyro } = useSensors();
const { update: updateFilter } = useComplementaryFilter();
const lastTimestampRef = useRef(0);
useEffect(() => {
const accelAngle = SlopeCalculator.calculateFromAccel(accel);
const now = accel.timestamp;
const dt = lastTimestampRef.current > 0 ? (now - lastTimestampRef.current) / 1000 : 0.02;
lastTimestampRef.current = now;
const filteredAngle = updateFilter(accelAngle, 0, dt);
setSlope(filteredAngle);
setSlopeGrade(SlopeCalculator.getSlopeGrade(filteredAngle));
}, [accel]);
return (
<div className="flex flex-col items-center justify-center h-full">
<p className="text-6xl font-bold">{slope.toFixed(1)}°</p>
<p className="text-lg text-gray-500">{slopeGrade}</p>
<p className="text-2xl text-amber-500">{SlopeCalculator.toPercentage(slope).toFixed(0)}%</p>
</div>
);
}
踩坑提醒
-
手机放置位置:手机放口袋里和手持的姿态不同,坡度计算结果也不同。建议在设置里让用户选择手机放置方式。
-
校准:开始滑雪前,让用户站在平地上校准一次,消除传感器偏差。
-
滤波参数:互补滤波的alpha参数(0.98)需要根据实际情况调整。alpha越大,越信任陀螺仪;越小,越信任加速度计。
-
电池消耗:两个传感器同时运行很耗电,建议在页面不可见时降低采样频率。
-
温度影响:低温环境下,传感器精度可能下降。建议在设置里提醒用户。
总结
这篇文章带你走了一遍传感器融合的完整流程:
- 传感器采集:同时订阅加速度计和陀螺仪
- 坡度计算:根据重力分量计算倾斜角度
- 互补滤波:融合两个传感器的数据
- 页面集成:把坡度数据展示出来
核心算法就一个:互补滤波。加速度计提供长期稳定的参考,陀螺仪提供短期精确的变化,两者融合得到最准确的结果。
两篇文章下来,雪痕的核心功能——GPS轨迹记录和传感器融合——就讲完了。如果你对滑雪App开发感兴趣,可以去鸿蒙应用市场下载雪痕体验一下,看看实际效果。
更多推荐



所有评论(0)