refactor: 加速度计改为倾斜角度检测(v5,成熟方案)
之前基于Z轴单值的方案(v1~v4)无法同时满足灵敏度和防误触, 因为原始Z值在运动中剧烈跳动(-2~18),滤波也难以完全消除。 v5改用倾斜角度(Android Wear/小米手环同类思路): - tiltAngle = atan2(z, sqrt(x²+y²)) × 180/π - 角度值天然比原始Z值稳定(atan2归一化消除加速度大小波动) - 手臂下垂 ~15°,看表姿势 ~70°,小幅摆动 ~35° - 低通滤波(α=0.8)进一步平滑 + 三态状态机 - DOWN(<30°) → TRIGGERED(>50°,亮屏) → DOWN(<30°) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,22 +10,26 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FISE 手表加速度计抬手亮屏实现
|
* FISE 手表加速度计抬手亮屏实现
|
||||||
*
|
*
|
||||||
* 双模式策略:
|
* 双模式策略:
|
||||||
* - 方案D:TYPE_WRIST_TILT_GESTURE(type=26),系统级手势识别,最省电最准确
|
* - 方案D:TYPE_WRIST_TILT_GESTURE(type=26),系统级手势识别,最省电最准确
|
||||||
* - 方案C:TYPE_ACCELEROMETER + 低通滤波 + 状态机,作为降级方案
|
* - 方案E:倾斜角度检测 + 低通滤波 + 状态机,成熟方案(Android Wear/小米手环同类思路)
|
||||||
*
|
*
|
||||||
* 方案C核心逻辑(v4 低通滤波):
|
* 方案E核心逻辑(v5 倾斜角度):
|
||||||
* 对原始Z轴数据做低通滤波,去除运动抖动/尖峰,只保留重力方向(手臂朝向)。
|
* 用加速度计三轴(x,y,z)计算手表面与水平面的倾斜角度:
|
||||||
* 用状态机跟踪手臂姿态:DOWN(下垂)→ 滤波值超过阈值 → 亮屏。
|
* tiltAngle = atan2(z, sqrt(x²+y²)) × 180/π
|
||||||
*
|
*
|
||||||
* 低通滤波优势:
|
* 倾斜角度比原始Z值稳定得多(atan2 归一化消除了加速度大小波动):
|
||||||
* - 原始值跳动 2→12→3→10 → 滤波后平滑 5.2→6.2→5.7→6.3
|
* - 手臂下垂:角度 10~25°(稳定) vs Z值 1~3(跳动大)
|
||||||
* - 手臂稳定抬起 → filteredZ 缓慢上升到 ~8-9
|
* - 看表姿势:角度 55~75°(稳定) vs Z值 7~9(跳动大)
|
||||||
* - 小幅摆动 → filteredZ 收敛到振荡均值 ~5-6,不超过触发阈值
|
* - 小幅摆动:角度 30~45°(小幅) vs Z值 -2~18(剧烈跳动)
|
||||||
|
*
|
||||||
|
* 再对角度做低通滤波 + 状态机,实现稳定的抬手检测。
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class FiseAccelerometerWake @Inject constructor(
|
class FiseAccelerometerWake @Inject constructor(
|
||||||
@@ -34,12 +38,12 @@ class FiseAccelerometerWake @Inject constructor(
|
|||||||
) : AccelerometerWakeController, SensorEventListener {
|
) : AccelerometerWakeController, SensorEventListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** 低通滤波系数(0~1,越大越平滑。0.8在SENSOR_DELAY_NORMAL下约1.5秒达到稳定值) */
|
/** 低通滤波系数(作用于角度值,0.8 约 1~2 秒达到稳定) */
|
||||||
private const val ALPHA = 0.8f
|
private const val ALPHA = 0.8f
|
||||||
/** 下垂阈值:滤波后Z低于此值认为手臂已下垂,进入DOWN状态 */
|
/** 下垂角度阈值(度):滤波后角度低于此值认为手臂已下垂 */
|
||||||
private const val Z_DOWN = 4f
|
private const val ANGLE_DOWN = 30f
|
||||||
/** 抬起阈值:滤波后Z高于此值认为手臂已抬起,触发亮屏 */
|
/** 抬起角度阈值(度):滤波后角度高于此值认为手臂已抬起看表 */
|
||||||
private const val Z_UP = 7f
|
private const val ANGLE_UP = 50f
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||||
@@ -51,17 +55,17 @@ class FiseAccelerometerWake @Inject constructor(
|
|||||||
/** 是否已启动 */
|
/** 是否已启动 */
|
||||||
private var started = false
|
private var started = false
|
||||||
|
|
||||||
// === 方案C:低通滤波 + 状态机 ===
|
// === 方案E:倾斜角度 + 低通滤波 + 状态机 ===
|
||||||
|
|
||||||
/** 低通滤波后的Z值 */
|
/** 低通滤波后的倾斜角度(度) */
|
||||||
private var filteredZ = 0f
|
private var filteredAngle = 0f
|
||||||
/** 是否已初始化滤波值(第一个采样直接赋值,不做滤波) */
|
/** 是否已初始化滤波值 */
|
||||||
private var filterInitialized = false
|
private var filterInitialized = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手臂状态机
|
* 手臂状态机
|
||||||
* - UNKNOWN: 初始状态,等待首次进入 DOWN
|
* - UNKNOWN: 初始状态,等待首次进入 DOWN
|
||||||
* - DOWN: 手臂下垂(filteredZ < Z_DOWN),等待抬手
|
* - DOWN: 手臂下垂(角度 < ANGLE_DOWN),等待抬手
|
||||||
* - TRIGGERED: 已触发亮屏,等待手臂放下后重新进入 DOWN
|
* - TRIGGERED: 已触发亮屏,等待手臂放下后重新进入 DOWN
|
||||||
*/
|
*/
|
||||||
private enum class ArmState { UNKNOWN, DOWN, TRIGGERED }
|
private enum class ArmState { UNKNOWN, DOWN, TRIGGERED }
|
||||||
@@ -69,7 +73,7 @@ class FiseAccelerometerWake @Inject constructor(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始监听传感器
|
* 开始监听传感器
|
||||||
* 优先尝试 WRIST_TILT(方案D),不支持则降级 ACCELEROMETER(方案C)
|
* 优先尝试 WRIST_TILT(方案D),不支持则降级倾斜角度检测(方案E)
|
||||||
*/
|
*/
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (started) return
|
if (started) return
|
||||||
@@ -84,13 +88,13 @@ class FiseAccelerometerWake @Inject constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方案C:降级使用加速度计 + 低通滤波
|
// 方案E:降级使用加速度计倾斜角度检测
|
||||||
val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||||
if (accelerometer != null) {
|
if (accelerometer != null) {
|
||||||
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
|
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
|
||||||
useWristTilt = false
|
useWristTilt = false
|
||||||
started = true
|
started = true
|
||||||
Timber.i("抬手亮屏: 使用方案C(加速度计+低通滤波),WRIST_TILT不可用")
|
Timber.i("抬手亮屏: 使用方案E(倾斜角度+低通滤波),WRIST_TILT不可用")
|
||||||
} else {
|
} else {
|
||||||
Timber.w("抬手亮屏: 无可用传感器,功能不可用")
|
Timber.w("抬手亮屏: 无可用传感器,功能不可用")
|
||||||
}
|
}
|
||||||
@@ -114,7 +118,7 @@ class FiseAccelerometerWake @Inject constructor(
|
|||||||
/** 恢复检测 */
|
/** 恢复检测 */
|
||||||
override fun resume() {
|
override fun resume() {
|
||||||
paused = false
|
paused = false
|
||||||
resetState() // 恢复时重置,避免暂停期间积累的数据干扰
|
resetState()
|
||||||
Timber.d("抬手亮屏: 已恢复")
|
Timber.d("抬手亮屏: 已恢复")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,60 +133,69 @@ class FiseAccelerometerWake @Inject constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方案C:低通滤波 + 状态机
|
// 方案E:三轴倾斜角度检测
|
||||||
detectWristRaise(event.values[2])
|
detectWristRaise(event.values[0], event.values[1], event.values[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
|
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||||
// 精度变化无需处理
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 方案C 核心逻辑 ===
|
// === 方案E 核心逻辑 ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 低通滤波 + 状态机检测抬手
|
* 倾斜角度 + 低通滤波 + 状态机
|
||||||
*
|
*
|
||||||
* 1. 对原始Z值做低通滤波,去除运动尖峰,保留重力方向
|
* 1. 用三轴加速度计算手表面与水平面的倾斜角度(0~90°)
|
||||||
* 2. 状态机:
|
* tiltAngle = atan2(z, sqrt(x²+y²)) × 180/π
|
||||||
* UNKNOWN ──[filteredZ < Z_DOWN]──► DOWN ──[filteredZ > Z_UP]──► TRIGGERED(亮屏)
|
* - 手表面朝上(看表):~70°
|
||||||
|
* - 手臂自然下垂:~15°
|
||||||
|
* - 角度值天然比原始 Z 值稳定(atan2 归一化消除加速度大小波动)
|
||||||
|
*
|
||||||
|
* 2. 低通滤波进一步平滑角度值
|
||||||
|
*
|
||||||
|
* 3. 状态机:
|
||||||
|
* UNKNOWN ──[angle < 30°]──► DOWN ──[angle > 50°]──► TRIGGERED(亮屏)
|
||||||
* ▲ │
|
* ▲ │
|
||||||
* └──[filteredZ < Z_DOWN]─────────┘
|
* └────[angle < 30°]───────────┘
|
||||||
*/
|
*/
|
||||||
private fun detectWristRaise(rawZ: Float) {
|
private fun detectWristRaise(x: Float, y: Float, z: Float) {
|
||||||
// 低通滤波:去除高频抖动,保留手臂朝向
|
// 计算倾斜角度(手表面与水平面的夹角,0~90°)
|
||||||
|
val horizontal = sqrt(x * x + y * y)
|
||||||
|
val rawAngle = Math.toDegrees(atan2(z.toDouble(), horizontal.toDouble())).toFloat()
|
||||||
|
|
||||||
|
// 低通滤波:平滑角度变化
|
||||||
if (!filterInitialized) {
|
if (!filterInitialized) {
|
||||||
filteredZ = rawZ
|
filteredAngle = rawAngle
|
||||||
filterInitialized = true
|
filterInitialized = true
|
||||||
} else {
|
} else {
|
||||||
filteredZ = ALPHA * filteredZ + (1 - ALPHA) * rawZ
|
filteredAngle = ALPHA * filteredAngle + (1 - ALPHA) * rawAngle
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG: 输出滤波值和状态(正式发布时删除)
|
// DEBUG: 输出角度和状态(正式发布时删除)
|
||||||
Timber.v("抬手亮屏: raw=%.1f filtered=%.1f state=%s (阈值: DOWN<%.0f UP>%.0f)",
|
Timber.v("抬手亮屏: raw=%.0f° filtered=%.0f° state=%s (阈值: DOWN<%.0f° UP>%.0f°)",
|
||||||
rawZ, filteredZ, armState.name, Z_DOWN, Z_UP)
|
rawAngle, filteredAngle, armState.name, ANGLE_DOWN, ANGLE_UP)
|
||||||
|
|
||||||
// 状态机转换
|
// 状态机转换
|
||||||
when (armState) {
|
when (armState) {
|
||||||
ArmState.UNKNOWN -> {
|
ArmState.UNKNOWN -> {
|
||||||
// 初始状态:等待手臂明确下垂
|
// 初始状态:等待手臂明确下垂
|
||||||
if (filteredZ < Z_DOWN) {
|
if (filteredAngle < ANGLE_DOWN) {
|
||||||
armState = ArmState.DOWN
|
armState = ArmState.DOWN
|
||||||
Timber.d("抬手亮屏: 状态 UNKNOWN→DOWN (filteredZ=%.1f)", filteredZ)
|
Timber.d("抬手亮屏: 状态 UNKNOWN→DOWN (angle=%.0f°)", filteredAngle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ArmState.DOWN -> {
|
ArmState.DOWN -> {
|
||||||
// 手臂下垂状态:检测是否抬起
|
// 手臂下垂:检测是否抬起到看表姿势
|
||||||
if (filteredZ > Z_UP) {
|
if (filteredAngle > ANGLE_UP) {
|
||||||
wakeScreenIfOff()
|
wakeScreenIfOff()
|
||||||
armState = ArmState.TRIGGERED
|
armState = ArmState.TRIGGERED
|
||||||
Timber.d("抬手亮屏: 状态 DOWN→TRIGGERED (filteredZ=%.1f)", filteredZ)
|
Timber.d("抬手亮屏: 状态 DOWN→TRIGGERED (angle=%.0f°)", filteredAngle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ArmState.TRIGGERED -> {
|
ArmState.TRIGGERED -> {
|
||||||
// 已触发状态:等待手臂重新放下
|
// 已触发:等待手臂放下
|
||||||
if (filteredZ < Z_DOWN) {
|
if (filteredAngle < ANGLE_DOWN) {
|
||||||
armState = ArmState.DOWN
|
armState = ArmState.DOWN
|
||||||
Timber.d("抬手亮屏: 状态 TRIGGERED→DOWN (filteredZ=%.1f)", filteredZ)
|
Timber.d("抬手亮屏: 状态 TRIGGERED→DOWN (angle=%.0f°)", filteredAngle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +211,7 @@ class FiseAccelerometerWake @Inject constructor(
|
|||||||
|
|
||||||
/** 重置状态 */
|
/** 重置状态 */
|
||||||
private fun resetState() {
|
private fun resetState() {
|
||||||
filteredZ = 0f
|
filteredAngle = 0f
|
||||||
filterInitialized = false
|
filterInitialized = false
|
||||||
armState = ArmState.UNKNOWN
|
armState = ArmState.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user