From 80f3982352b7e44a6a8367ced3600d706ad39e73 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Wed, 24 Jun 2026 12:18:38 +0900 Subject: [PATCH] =?UTF-8?q?fix(walking):=20sit-to-stand=20burst=20window?= =?UTF-8?q?=20leak=20=E2=80=94=20pitch=20transition=20=EC=8B=9C=20buffer?= =?UTF-8?q?=20=EB=B9=84=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: 앉아있다 일어서기만 해도 WALKING false-trigger (두 번 연속 발생). 원인 (로그 분석): 앉아있음(pitch=20-30°) → 일어남 burst (gyro 90-220 dps) → 서있음(pitch=82°) 타임라인: T=0 pitch=37° wmean=49 (burst 시작, pitch < 45 → pitchOk=false) T=1 pitch=14° wmean=110 (더 큰 burst, pitchOk=false) T=2 pitch=32° wmean=174 (burst 최고, pitchOk=false) T=3 pitch=82° wmean=190 ← pitchOk=true 된 순간 window 에 burst 가득 → start cond 즉시 만족, 카운트 시작 T=4 pitch=50° wmean=82 (burst 빠지는 중) T=5 pitch=46° wmean=12 (거의 빠짐, 그래도 > Start Wmean(4)) T=6 pitch=57° wmean=24 → "WALKING start (held 3091ms)" ❌ 핵심: pitch 가 45° 통과한 순간 window 안엔 이미 burst data. burst 가 다 빠지기 전에 (~1.5초) + 3초 hold 누적 = false-trigger. 수정: WalkingDetector 에 lastPitchOk 추적 + false→true 전이 시 buffer.clear() + startCondSinceMs=null. if (pitchOk && !lastPitchOk && !walking) { buffer.clear() // burst data 폐기 startCondSinceMs = null // hold 카운트 리셋 } 결과: pitch 가 45° 넘는 순간부터 fresh sample 만 누적. 사용자가 일어서서 가만히 있으면 wmean=1-2 (정상 정지값) → cond 안 met → trigger 안 됨. 실제 보행 시작하면 새 wmean 이 30-50+ 까지 올라가 → 3초 hold → trigger. walking 중에는 pitchOk false→true 가 발생해도 (잠깐 앉았다 일어남 등) buffer 안 비움 (walking 상태 유지 — keep cond 가 처리). Co-Authored-By: Claude Opus 4.7 --- .../managers/WalkingDetector.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/managers/WalkingDetector.kt b/app/src/main/java/com/example/medilightv2android/managers/WalkingDetector.kt index 0fbe542..341d5b3 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/WalkingDetector.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/WalkingDetector.kt @@ -44,6 +44,8 @@ class WalkingDetector { private var startCondSinceMs: Long? = null /** WALKING 에서 유지 조건 마지막 만족 시각. */ private var keepCondLastOkMs: Long? = null + /** 직전 cycle 의 pitchOk — false→true 전이 시 buffer reset (sit-to-stand burst 차단). */ + private var lastPitchOk: Boolean = false val isWalking: Boolean get() = walking @@ -52,6 +54,7 @@ class WalkingDetector { walking = false startCondSinceMs = null keepCondLastOkMs = null + lastPitchOk = false } /** @@ -59,6 +62,19 @@ class WalkingDetector { * pitchDeg = 같은 cycle 의 pitch (ImuPostureClassifier 가 계산한 값). */ fun update(samples: List, pitchDeg: Float, nowMs: Long): Snapshot { + val minPitch = GreenZoneConstants.walkingMinPitchDeg + val pitchOk = abs(pitchDeg) > minPitch + + // Pitch false→true 전이 (sit-to-stand 등) — buffer 비움. + // 이유: 일어남 burst 가 pitch<45° 일 때 발생 → window 에 누적 → pitch 가 통과한 + // 순간 wmean 이 이미 큰 값 → 즉시 start cond 만족 → 가짜 walking trigger. + // pitch 가 true 가 된 직후엔 새 데이터만으로 measure 하도록. + if (pitchOk && !lastPitchOk && !walking) { + buffer.clear() + startCondSinceMs = null + } + lastPitchOk = pitchOk + // 1) sample buffer 에 추가 (timestamp 단순 분배 — cycle 내 균등 간격 가정) val windowMs = GreenZoneConstants.walkingWindowMs if (samples.isNotEmpty()) { @@ -89,10 +105,7 @@ class WalkingDetector { } val wstd = sqrt(sqSum / n) - // 4) state machine - val minPitch = GreenZoneConstants.walkingMinPitchDeg - val pitchOk = abs(pitchDeg) > minPitch - + // 4) state machine (pitchOk 은 위에서 이미 계산됨) if (!walking) { // IDLE → WALKING 검사 (4-2) val cond = pitchOk &&