From f35bbfc1e91ae6c97ea64d21d0b6c3591802ddbb Mon Sep 17 00:00:00 2001 From: dongliang Date: Thu, 30 Apr 2026 18:29:39 +0930 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=8A=A0=E9=80=9F=E5=BA=A6?= =?UTF-8?q?=E8=AE=A1=E6=94=B9=E4=B8=BA=E5=80=BE=E6=96=9C=E8=A7=92=E5=BA=A6?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=EF=BC=88v5=EF=BC=8C=E6=88=90=E7=86=9F?= =?UTF-8?q?=E6=96=B9=E6=A1=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前基于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) --- .../device/sensor/FiseAccelerometerWake.kt | 117 ++++++++++-------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseAccelerometerWake.kt b/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseAccelerometerWake.kt index 2053051..0c7da74 100644 --- a/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseAccelerometerWake.kt +++ b/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseAccelerometerWake.kt @@ -10,22 +10,26 @@ import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.atan2 +import kotlin.math.sqrt /** * FISE 手表加速度计抬手亮屏实现 * * 双模式策略: * - 方案D:TYPE_WRIST_TILT_GESTURE(type=26),系统级手势识别,最省电最准确 - * - 方案C:TYPE_ACCELEROMETER + 低通滤波 + 状态机,作为降级方案 + * - 方案E:倾斜角度检测 + 低通滤波 + 状态机,成熟方案(Android Wear/小米手环同类思路) * - * 方案C核心逻辑(v4 低通滤波): - * 对原始Z轴数据做低通滤波,去除运动抖动/尖峰,只保留重力方向(手臂朝向)。 - * 用状态机跟踪手臂姿态:DOWN(下垂)→ 滤波值超过阈值 → 亮屏。 + * 方案E核心逻辑(v5 倾斜角度): + * 用加速度计三轴(x,y,z)计算手表面与水平面的倾斜角度: + * tiltAngle = atan2(z, sqrt(x²+y²)) × 180/π * - * 低通滤波优势: - * - 原始值跳动 2→12→3→10 → 滤波后平滑 5.2→6.2→5.7→6.3 - * - 手臂稳定抬起 → filteredZ 缓慢上升到 ~8-9 - * - 小幅摆动 → filteredZ 收敛到振荡均值 ~5-6,不超过触发阈值 + * 倾斜角度比原始Z值稳定得多(atan2 归一化消除了加速度大小波动): + * - 手臂下垂:角度 10~25°(稳定) vs Z值 1~3(跳动大) + * - 看表姿势:角度 55~75°(稳定) vs Z值 7~9(跳动大) + * - 小幅摆动:角度 30~45°(小幅) vs Z值 -2~18(剧烈跳动) + * + * 再对角度做低通滤波 + 状态机,实现稳定的抬手检测。 */ @Singleton class FiseAccelerometerWake @Inject constructor( @@ -34,12 +38,12 @@ class FiseAccelerometerWake @Inject constructor( ) : AccelerometerWakeController, SensorEventListener { companion object { - /** 低通滤波系数(0~1,越大越平滑。0.8在SENSOR_DELAY_NORMAL下约1.5秒达到稳定值) */ + /** 低通滤波系数(作用于角度值,0.8 约 1~2 秒达到稳定) */ private const val ALPHA = 0.8f - /** 下垂阈值:滤波后Z低于此值认为手臂已下垂,进入DOWN状态 */ - private const val Z_DOWN = 4f - /** 抬起阈值:滤波后Z高于此值认为手臂已抬起,触发亮屏 */ - private const val Z_UP = 7f + /** 下垂角度阈值(度):滤波后角度低于此值认为手臂已下垂 */ + private const val ANGLE_DOWN = 30f + /** 抬起角度阈值(度):滤波后角度高于此值认为手臂已抬起看表 */ + private const val ANGLE_UP = 50f } private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager @@ -51,25 +55,25 @@ class FiseAccelerometerWake @Inject constructor( /** 是否已启动 */ private var started = false - // === 方案C:低通滤波 + 状态机 === + // === 方案E:倾斜角度 + 低通滤波 + 状态机 === - /** 低通滤波后的Z值 */ - private var filteredZ = 0f - /** 是否已初始化滤波值(第一个采样直接赋值,不做滤波) */ + /** 低通滤波后的倾斜角度(度) */ + private var filteredAngle = 0f + /** 是否已初始化滤波值 */ private var filterInitialized = false /** * 手臂状态机 - * - UNKNOWN: 初始状态,等待首次进入DOWN - * - DOWN: 手臂下垂(filteredZ < Z_DOWN),等待抬手 - * - TRIGGERED: 已触发亮屏,等待手臂放下后重新进入DOWN + * - UNKNOWN: 初始状态,等待首次进入 DOWN + * - DOWN: 手臂下垂(角度 < ANGLE_DOWN),等待抬手 + * - TRIGGERED: 已触发亮屏,等待手臂放下后重新进入 DOWN */ private enum class ArmState { UNKNOWN, DOWN, TRIGGERED } private var armState = ArmState.UNKNOWN /** * 开始监听传感器 - * 优先尝试 WRIST_TILT(方案D),不支持则降级 ACCELEROMETER(方案C) + * 优先尝试 WRIST_TILT(方案D),不支持则降级倾斜角度检测(方案E) */ override fun start() { if (started) return @@ -84,13 +88,13 @@ class FiseAccelerometerWake @Inject constructor( return } - // 方案C:降级使用加速度计 + 低通滤波 + // 方案E:降级使用加速度计倾斜角度检测 val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) if (accelerometer != null) { sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL) useWristTilt = false started = true - Timber.i("抬手亮屏: 使用方案C(加速度计+低通滤波),WRIST_TILT不可用") + Timber.i("抬手亮屏: 使用方案E(倾斜角度+低通滤波),WRIST_TILT不可用") } else { Timber.w("抬手亮屏: 无可用传感器,功能不可用") } @@ -114,7 +118,7 @@ class FiseAccelerometerWake @Inject constructor( /** 恢复检测 */ override fun resume() { paused = false - resetState() // 恢复时重置,避免暂停期间积累的数据干扰 + resetState() Timber.d("抬手亮屏: 已恢复") } @@ -129,60 +133,69 @@ class FiseAccelerometerWake @Inject constructor( return } - // 方案C:低通滤波 + 状态机 - detectWristRaise(event.values[2]) + // 方案E:三轴倾斜角度检测 + 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值做低通滤波,去除运动尖峰,保留重力方向 - * 2. 状态机: - * UNKNOWN ──[filteredZ < Z_DOWN]──► DOWN ──[filteredZ > Z_UP]──► TRIGGERED(亮屏) - * ▲ │ - * └──[filteredZ < Z_DOWN]─────────┘ + * 1. 用三轴加速度计算手表面与水平面的倾斜角度(0~90°) + * tiltAngle = atan2(z, sqrt(x²+y²)) × 180/π + * - 手表面朝上(看表):~70° + * - 手臂自然下垂:~15° + * - 角度值天然比原始 Z 值稳定(atan2 归一化消除加速度大小波动) + * + * 2. 低通滤波进一步平滑角度值 + * + * 3. 状态机: + * UNKNOWN ──[angle < 30°]──► DOWN ──[angle > 50°]──► TRIGGERED(亮屏) + * ▲ │ + * └────[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) { - filteredZ = rawZ + filteredAngle = rawAngle filterInitialized = true } else { - filteredZ = ALPHA * filteredZ + (1 - ALPHA) * rawZ + filteredAngle = ALPHA * filteredAngle + (1 - ALPHA) * rawAngle } - // DEBUG: 输出滤波值和状态(正式发布时删除) - Timber.v("抬手亮屏: raw=%.1f filtered=%.1f state=%s (阈值: DOWN<%.0f UP>%.0f)", - rawZ, filteredZ, armState.name, Z_DOWN, Z_UP) + // DEBUG: 输出角度和状态(正式发布时删除) + Timber.v("抬手亮屏: raw=%.0f° filtered=%.0f° state=%s (阈值: DOWN<%.0f° UP>%.0f°)", + rawAngle, filteredAngle, armState.name, ANGLE_DOWN, ANGLE_UP) // 状态机转换 when (armState) { ArmState.UNKNOWN -> { // 初始状态:等待手臂明确下垂 - if (filteredZ < Z_DOWN) { + if (filteredAngle < ANGLE_DOWN) { armState = ArmState.DOWN - Timber.d("抬手亮屏: 状态 UNKNOWN→DOWN (filteredZ=%.1f)", filteredZ) + Timber.d("抬手亮屏: 状态 UNKNOWN→DOWN (angle=%.0f°)", filteredAngle) } } ArmState.DOWN -> { - // 手臂下垂状态:检测是否抬起 - if (filteredZ > Z_UP) { + // 手臂下垂:检测是否抬起到看表姿势 + if (filteredAngle > ANGLE_UP) { wakeScreenIfOff() armState = ArmState.TRIGGERED - Timber.d("抬手亮屏: 状态 DOWN→TRIGGERED (filteredZ=%.1f)", filteredZ) + Timber.d("抬手亮屏: 状态 DOWN→TRIGGERED (angle=%.0f°)", filteredAngle) } } ArmState.TRIGGERED -> { - // 已触发状态:等待手臂重新放下 - if (filteredZ < Z_DOWN) { + // 已触发:等待手臂放下 + if (filteredAngle < ANGLE_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() { - filteredZ = 0f + filteredAngle = 0f filterInitialized = false armState = ArmState.UNKNOWN }