fix: 加速度计改为最小值跳变检测(v3),大幅提升灵敏度
v1/v2的滑动窗口均值方案需要连续3个低值采样才能触发, 实测抬手动作太快(<0.5秒)导致大量漏触发。 v3改为追踪近期5个采样的最小值: - 近期有任一值<5(手臂曾下垂)且当前值>=7(手臂已抬起)→触发 - 只需1个低值采样即可,灵敏度大幅提升 - 触发后10个采样(~2秒)冷却期防连续触发 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,13 +18,16 @@ import javax.inject.Singleton
|
||||
* - 方案D:TYPE_WRIST_TILT_GESTURE(type=26),系统级手势识别,最省电最准确
|
||||
* - 方案C:TYPE_ACCELEROMETER Z轴变化趋势检测,作为降级方案
|
||||
*
|
||||
* 方案C核心逻辑:
|
||||
* 检测Z轴从低值(<3,手臂下垂)到高值(≥6,手臂抬起)的变化趋势。
|
||||
* 与旧版简单的Z≥5判断相比,可防止:
|
||||
* - 手表平放桌面常亮(Z一直≈9.8,无"低→高"变化)
|
||||
* - 走路手臂摆动频繁亮灭(Z波动但无持续趋势)
|
||||
* 方案C核心逻辑(v3 最小值跳变检测):
|
||||
* 追踪近期5个采样(≈1秒)的最小Z值。当近期有低值(<5,手臂曾下垂)
|
||||
* 且当前Z值高(≥7,手臂已抬起)时判定为抬手。
|
||||
* 只需1个低值采样即可触发,比均值方案灵敏得多。
|
||||
* 触发后2秒冷却期防连续触发。
|
||||
*
|
||||
* 参数来源:baseline/04 — Z轴唤醒阈值≥5(home.vue:122),加迟滞调整为低<3/高≥6
|
||||
* 与旧版和v1/v2方案的改进:
|
||||
* - 旧版Z≥5:平放桌面常亮、走路闪烁
|
||||
* - v1/v2均值对比:需要连续3个低值,抬手动作太快时漏触发
|
||||
* - v3最小值:只需1个低值,更符合真实抬手动作的数据特征
|
||||
*/
|
||||
@Singleton
|
||||
class FiseAccelerometerWake @Inject constructor(
|
||||
@@ -33,14 +36,14 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
) : AccelerometerWakeController, SensorEventListener {
|
||||
|
||||
companion object {
|
||||
/** 方案C:Z轴低位阈值(手臂下垂判定),实测下垂稳定值0-3 */
|
||||
private const val Z_LOW_THRESHOLD = 4f
|
||||
/** 方案C:Z轴高位阈值(手臂抬起判定),实测抬手过渡期均值4.5-6.5,稳定值8-9 */
|
||||
private const val Z_HIGH_THRESHOLD = 5f
|
||||
/** 方案C:滑动窗口大小(前半+后半各3个采样,SENSOR_DELAY_NORMAL下约1.2秒) */
|
||||
private const val WINDOW_SIZE = 6
|
||||
/** 方案C:前半窗口大小 */
|
||||
private const val HALF_WINDOW = WINDOW_SIZE / 2
|
||||
/** 方案C:近期最小值窗口大小(5个采样≈1秒,用于追踪"手臂曾经放下过") */
|
||||
private const val MIN_WINDOW_SIZE = 5
|
||||
/** 方案C:最小值阈值,近期有采样低于此值才认为"手臂曾下垂" */
|
||||
private const val Z_MIN_THRESHOLD = 5f
|
||||
/** 方案C:当前值阈值,超过此值认为"手臂已抬起" */
|
||||
private const val Z_CURRENT_THRESHOLD = 7f
|
||||
/** 方案C:触发后冷却采样数(防连续触发,10个≈2秒) */
|
||||
private const val COOLDOWN_SAMPLES = 10
|
||||
}
|
||||
|
||||
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
@@ -52,13 +55,15 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
/** 是否已启动 */
|
||||
private var started = false
|
||||
|
||||
// === 方案C:Z轴滑动窗口 ===
|
||||
/** 固定大小的环形缓冲区,存储最近的Z轴采样值 */
|
||||
private val zWindow = FloatArray(WINDOW_SIZE)
|
||||
// === 方案C:最小值追踪 ===
|
||||
/** 环形缓冲区,存储最近N个Z轴采样值,用于找最小值 */
|
||||
private val zWindow = FloatArray(MIN_WINDOW_SIZE) { Float.MAX_VALUE }
|
||||
/** 当前写入位置 */
|
||||
private var windowIndex = 0
|
||||
/** 窗口是否已填满(至少6个采样后才开始判断) */
|
||||
/** 窗口是否已填满 */
|
||||
private var windowFilled = false
|
||||
/** 触发后冷却计数器 */
|
||||
private var cooldownCount = 0
|
||||
|
||||
/**
|
||||
* 开始监听传感器
|
||||
@@ -133,51 +138,46 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
// === 方案C 核心逻辑 ===
|
||||
|
||||
/**
|
||||
* Z轴变化趋势检测
|
||||
* 用环形缓冲区存储最近6个Z轴采样值,分前半(旧)和后半(新)各3个:
|
||||
* - 前3个均值 < 3(手臂下垂状态)
|
||||
* - 后3个均值 ≥ 6(手臂抬起状态)
|
||||
* 满足条件 = 检测到"抬手"动作
|
||||
* 最小值跳变检测
|
||||
* 追踪近期(最近5个采样≈1秒)的最小Z值:
|
||||
* - 如果近期最小值 < 5(说明手臂曾经放下过)
|
||||
* - 并且当前Z值 ≥ 7(说明手臂已抬起)
|
||||
* → 判定为"抬手"动作
|
||||
*
|
||||
* 优点:只需一个低值采样即可触发,比均值方案灵敏得多
|
||||
*/
|
||||
private fun detectWristRaise(z: Float) {
|
||||
// 冷却期:刚触发过,等待冷却结束
|
||||
if (cooldownCount > 0) {
|
||||
cooldownCount--
|
||||
return
|
||||
}
|
||||
|
||||
// 写入环形缓冲区
|
||||
zWindow[windowIndex % WINDOW_SIZE] = z
|
||||
val pos = windowIndex % MIN_WINDOW_SIZE
|
||||
zWindow[pos] = z
|
||||
windowIndex++
|
||||
|
||||
// 窗口未填满,等待更多采样
|
||||
if (!windowFilled) {
|
||||
if (windowIndex >= WINDOW_SIZE) {
|
||||
windowFilled = true
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if (!windowFilled && windowIndex >= MIN_WINDOW_SIZE) {
|
||||
windowFilled = true
|
||||
}
|
||||
if (!windowFilled) return
|
||||
|
||||
// 计算前半(较旧的3个采样)和后半(较新的3个采样)的均值
|
||||
// 环形缓冲区中,当前写入位置的前 HALF_WINDOW 个是最新的,再前 HALF_WINDOW 个是较旧的
|
||||
val currentPos = windowIndex % WINDOW_SIZE
|
||||
var oldSum = 0f
|
||||
var newSum = 0f
|
||||
for (i in 0 until HALF_WINDOW) {
|
||||
// 较旧的3个:从 currentPos 往前数第 6,5,4 个位置
|
||||
val oldIdx = (currentPos - WINDOW_SIZE + i + WINDOW_SIZE) % WINDOW_SIZE
|
||||
// 较新的3个:从 currentPos 往前数第 3,2,1 个位置
|
||||
val newIdx = (currentPos - HALF_WINDOW + i + WINDOW_SIZE) % WINDOW_SIZE
|
||||
oldSum += zWindow[oldIdx]
|
||||
newSum += zWindow[newIdx]
|
||||
// 找近期最小值
|
||||
var recentMin = Float.MAX_VALUE
|
||||
for (i in 0 until MIN_WINDOW_SIZE) {
|
||||
if (zWindow[i] < recentMin) recentMin = zWindow[i]
|
||||
}
|
||||
val oldAvg = oldSum / HALF_WINDOW
|
||||
val newAvg = newSum / HALF_WINDOW
|
||||
|
||||
// DEBUG: 输出每次计算结果,用于调参(正式发布时删除)
|
||||
Timber.v("抬手亮屏: Z=%.1f oldAvg=%.1f newAvg=%.1f (阈值: old<%.0f AND new>=%.0f)",
|
||||
zWindow[(windowIndex - 1 + WINDOW_SIZE) % WINDOW_SIZE],
|
||||
oldAvg, newAvg, Z_LOW_THRESHOLD, Z_HIGH_THRESHOLD)
|
||||
Timber.v("抬手亮屏: Z=%.1f recentMin=%.1f (阈值: min<%.0f AND cur>=%.0f)",
|
||||
z, recentMin, Z_MIN_THRESHOLD, Z_CURRENT_THRESHOLD)
|
||||
|
||||
// 判断:从低位(下垂)到高位(抬起)的变化趋势
|
||||
if (oldAvg < Z_LOW_THRESHOLD && newAvg >= Z_HIGH_THRESHOLD) {
|
||||
// 判断:近期有低值(手臂曾下垂)且当前值高(手臂已抬起)
|
||||
if (recentMin < Z_MIN_THRESHOLD && z >= Z_CURRENT_THRESHOLD) {
|
||||
wakeScreenIfOff()
|
||||
// 触发后清空窗口,防止连续触发
|
||||
// 触发后进入冷却期 + 清空窗口
|
||||
cooldownCount = COOLDOWN_SAMPLES
|
||||
resetWindow()
|
||||
}
|
||||
}
|
||||
@@ -190,10 +190,10 @@ class FiseAccelerometerWake @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置滑动窗口 */
|
||||
/** 重置窗口 */
|
||||
private fun resetWindow() {
|
||||
windowIndex = 0
|
||||
windowFilled = false
|
||||
zWindow.fill(0f)
|
||||
zWindow.fill(Float.MAX_VALUE)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user