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 0f525a6..2053051 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 @@ -16,18 +16,16 @@ import javax.inject.Singleton * * 双模式策略: * - 方案D:TYPE_WRIST_TILT_GESTURE(type=26),系统级手势识别,最省电最准确 - * - 方案C:TYPE_ACCELEROMETER Z轴变化趋势检测,作为降级方案 + * - 方案C:TYPE_ACCELEROMETER + 低通滤波 + 状态机,作为降级方案 * - * 方案C核心逻辑(v3 最小值跳变检测): - * 追踪近期5个采样(≈1秒)的最小Z值。当近期有低值(<5,手臂曾下垂) - * 且当前Z值高(≥7,手臂已抬起)时判定为抬手。 - * 只需1个低值采样即可触发,比均值方案灵敏得多。 - * 触发后2秒冷却期防连续触发。 + * 方案C核心逻辑(v4 低通滤波): + * 对原始Z轴数据做低通滤波,去除运动抖动/尖峰,只保留重力方向(手臂朝向)。 + * 用状态机跟踪手臂姿态:DOWN(下垂)→ 滤波值超过阈值 → 亮屏。 * - * 与旧版和v1/v2方案的改进: - * - 旧版Z≥5:平放桌面常亮、走路闪烁 - * - v1/v2均值对比:需要连续3个低值,抬手动作太快时漏触发 - * - v3最小值:只需1个低值,更符合真实抬手动作的数据特征 + * 低通滤波优势: + * - 原始值跳动 2→12→3→10 → 滤波后平滑 5.2→6.2→5.7→6.3 + * - 手臂稳定抬起 → filteredZ 缓慢上升到 ~8-9 + * - 小幅摆动 → filteredZ 收敛到振荡均值 ~5-6,不超过触发阈值 */ @Singleton class FiseAccelerometerWake @Inject constructor( @@ -36,16 +34,12 @@ class FiseAccelerometerWake @Inject constructor( ) : AccelerometerWakeController, SensorEventListener { companion object { - /** 方案C:近期最小值窗口大小(5个采样≈1秒,用于追踪"手臂曾经放下过") */ - private const val MIN_WINDOW_SIZE = 5 - /** 方案C:最小值阈值,近期有采样低于此值才认为"手臂曾明确下垂"(实测:小幅摆动Z≈5-6,下垂Z≈1-3) */ - private const val Z_MIN_THRESHOLD = 3f - /** 方案C:当前值阈值,超过此值认为"手臂已明确抬起"(实测:抬手稳定Z≈7.5-9,偶尔波动到7.5) */ - private const val Z_CURRENT_THRESHOLD = 7.5f - /** 方案C:连续高值计数要求(需连续N个采样≥阈值才触发,防摆动尖峰误触发) */ - private const val HIGH_COUNT_REQUIRED = 3 - /** 方案C:触发后冷却采样数(防连续触发,10个≈2秒) */ - private const val COOLDOWN_SAMPLES = 10 + /** 低通滤波系数(0~1,越大越平滑。0.8在SENSOR_DELAY_NORMAL下约1.5秒达到稳定值) */ + private const val ALPHA = 0.8f + /** 下垂阈值:滤波后Z低于此值认为手臂已下垂,进入DOWN状态 */ + private const val Z_DOWN = 4f + /** 抬起阈值:滤波后Z高于此值认为手臂已抬起,触发亮屏 */ + private const val Z_UP = 7f } private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager @@ -57,17 +51,21 @@ class FiseAccelerometerWake @Inject constructor( /** 是否已启动 */ private var started = false - // === 方案C:最小值追踪 === - /** 环形缓冲区,存储最近N个Z轴采样值,用于找最小值 */ - private val zWindow = FloatArray(MIN_WINDOW_SIZE) { Float.MAX_VALUE } - /** 当前写入位置 */ - private var windowIndex = 0 - /** 窗口是否已填满 */ - private var windowFilled = false - /** 触发后冷却计数器 */ - private var cooldownCount = 0 - /** 连续高值计数器(Z >= 阈值的连续采样数) */ - private var highCount = 0 + // === 方案C:低通滤波 + 状态机 === + + /** 低通滤波后的Z值 */ + private var filteredZ = 0f + /** 是否已初始化滤波值(第一个采样直接赋值,不做滤波) */ + private var filterInitialized = false + + /** + * 手臂状态机 + * - UNKNOWN: 初始状态,等待首次进入DOWN + * - DOWN: 手臂下垂(filteredZ < Z_DOWN),等待抬手 + * - TRIGGERED: 已触发亮屏,等待手臂放下后重新进入DOWN + */ + private enum class ArmState { UNKNOWN, DOWN, TRIGGERED } + private var armState = ArmState.UNKNOWN /** * 开始监听传感器 @@ -86,13 +84,13 @@ class FiseAccelerometerWake @Inject constructor( return } - // 方案C:降级使用加速度计Z轴变化趋势检测 + // 方案C:降级使用加速度计 + 低通滤波 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(加速度计Z轴变化趋势),WRIST_TILT不可用") + Timber.i("抬手亮屏: 使用方案C(加速度计+低通滤波),WRIST_TILT不可用") } else { Timber.w("抬手亮屏: 无可用传感器,功能不可用") } @@ -103,7 +101,7 @@ class FiseAccelerometerWake @Inject constructor( if (!started) return sensorManager.unregisterListener(this) started = false - resetWindow() + resetState() Timber.d("抬手亮屏: 已停止") } @@ -116,7 +114,7 @@ class FiseAccelerometerWake @Inject constructor( /** 恢复检测 */ override fun resume() { paused = false - resetWindow() // 恢复时清空窗口,避免暂停期间积累的数据干扰判断 + resetState() // 恢复时重置,避免暂停期间积累的数据干扰 Timber.d("抬手亮屏: 已恢复") } @@ -131,7 +129,7 @@ class FiseAccelerometerWake @Inject constructor( return } - // 方案C:Z轴变化趋势检测 + // 方案C:低通滤波 + 状态机 detectWristRaise(event.values[2]) } @@ -142,57 +140,51 @@ class FiseAccelerometerWake @Inject constructor( // === 方案C 核心逻辑 === /** - * 最小值 + 连续高值检测 + * 低通滤波 + 状态机检测抬手 * - * 两个条件同时满足才触发: - * 1. 近期5个采样中有值 < 3(手臂曾明确下垂) - * 2. 当前连续 3 个采样都 ≥ 8(手臂已抬起并稳定,非瞬间尖峰) - * - * 连续高值要求可区分: - * - 真正抬手:Z 稳定在 8-9 持续多个采样 → 连续 3 个 ≥ 8 ✅ - * - 小幅摆动:Z 在 0-10 间剧烈跳动,尖峰后立即回落 → 连续不到 3 个 ❌ + * 1. 对原始Z值做低通滤波,去除运动尖峰,保留重力方向 + * 2. 状态机: + * UNKNOWN ──[filteredZ < Z_DOWN]──► DOWN ──[filteredZ > Z_UP]──► TRIGGERED(亮屏) + * ▲ │ + * └──[filteredZ < Z_DOWN]─────────┘ */ - private fun detectWristRaise(z: Float) { - // 冷却期:刚触发过,等待冷却结束 - if (cooldownCount > 0) { - cooldownCount-- - return - } - - // 写入环形缓冲区(追踪近期最小值) - val pos = windowIndex % MIN_WINDOW_SIZE - zWindow[pos] = z - windowIndex++ - - if (!windowFilled && windowIndex >= MIN_WINDOW_SIZE) { - windowFilled = true - } - if (!windowFilled) return - - // 更新连续高值计数 - if (z >= Z_CURRENT_THRESHOLD) { - highCount++ + private fun detectWristRaise(rawZ: Float) { + // 低通滤波:去除高频抖动,保留手臂朝向 + if (!filterInitialized) { + filteredZ = rawZ + filterInitialized = true } else { - highCount = 0 + filteredZ = ALPHA * filteredZ + (1 - ALPHA) * rawZ } - // 找近期最小值 - var recentMin = Float.MAX_VALUE - for (i in 0 until MIN_WINDOW_SIZE) { - if (zWindow[i] < recentMin) recentMin = zWindow[i] - } + // DEBUG: 输出滤波值和状态(正式发布时删除) + Timber.v("抬手亮屏: raw=%.1f filtered=%.1f state=%s (阈值: DOWN<%.0f UP>%.0f)", + rawZ, filteredZ, armState.name, Z_DOWN, Z_UP) - // DEBUG: 输出每次计算结果,用于调参(正式发布时删除) - Timber.v("抬手亮屏: Z=%.1f recentMin=%.1f highCount=%d (阈值: min<%.0f AND %d连续>=%.0f)", - z, recentMin, highCount, Z_MIN_THRESHOLD, HIGH_COUNT_REQUIRED, Z_CURRENT_THRESHOLD) - - // 判断:近期有低值(手臂曾下垂)且连续高值达标(手臂已稳定抬起) - if (recentMin < Z_MIN_THRESHOLD && highCount >= HIGH_COUNT_REQUIRED) { - wakeScreenIfOff() - // 触发后进入冷却期 + 清空状态 - cooldownCount = COOLDOWN_SAMPLES - highCount = 0 - resetWindow() + // 状态机转换 + when (armState) { + ArmState.UNKNOWN -> { + // 初始状态:等待手臂明确下垂 + if (filteredZ < Z_DOWN) { + armState = ArmState.DOWN + Timber.d("抬手亮屏: 状态 UNKNOWN→DOWN (filteredZ=%.1f)", filteredZ) + } + } + ArmState.DOWN -> { + // 手臂下垂状态:检测是否抬起 + if (filteredZ > Z_UP) { + wakeScreenIfOff() + armState = ArmState.TRIGGERED + Timber.d("抬手亮屏: 状态 DOWN→TRIGGERED (filteredZ=%.1f)", filteredZ) + } + } + ArmState.TRIGGERED -> { + // 已触发状态:等待手臂重新放下 + if (filteredZ < Z_DOWN) { + armState = ArmState.DOWN + Timber.d("抬手亮屏: 状态 TRIGGERED→DOWN (filteredZ=%.1f)", filteredZ) + } + } } } @@ -204,10 +196,10 @@ class FiseAccelerometerWake @Inject constructor( } } - /** 重置窗口 */ - private fun resetWindow() { - windowIndex = 0 - windowFilled = false - zWindow.fill(Float.MAX_VALUE) + /** 重置状态 */ + private fun resetState() { + filteredZ = 0f + filterInitialized = false + armState = ArmState.UNKNOWN } }