fix(walking): chained failure — acc gate fail 시 walking 잃던 버그

증상: 로그캣 분석 — 활발히 걷는 중 (accMag > 1.2g) 일수록 화면에 안 보임.
  11:24:03 NON_LYING → WALKING 성공
  11:24:04 accMag=1.80 → UNKNOWN, walking 도 grace 들어감
  11:24:05 grace 2초 초과 → walking 종료
  → 활발 보행 = 가장 위험한 순간에 알람 사라짐 (정반대)

원인 두 가지:
  1) Classifier 가 acc gate fail 시 pitch=0 반환
     → WalkingDetector 가 pitch=0 받아서 pitchOk=false → grace 진입
  2) Composite 로직: r.posture==UNKNOWN 이면 WALKING 무시하고 UNKNOWN 표시

수정:
  1) ImuPostureClassifier: pitch 계산을 acc gate 이전으로 옮김
     → UNKNOWN Result 에도 실제 pitch 값 포함
     → WalkingDetector 가 정확한 pitch 로 grace 판단 가능

  2) PiezoMonitoringView composite 순서 변경:
     when {
       isWalkingNow -> WALKING          // 우선 (자체 게이트 신뢰)
       r.posture==UNKNOWN -> UNKNOWN
       else -> r.posture
     }
     walking detector 가 walking 이라고 하면 acc gate 실패해도 표시.
     walking 자체에 시간/wmean/wstd 게이트 있어 false-positive 거의 없음.

테스트 시나리오:
  정지 lying → 흔들면서 걷기 (pitch>45°) → walking 검출 → 격하게 흔들기
  (accMag>1.2g) 도 → "Walking" 계속 표시 + 빨간 팝업 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 11:27:58 +09:00
parent ecdec7c97c
commit 6bf399c7b9
2 changed files with 8 additions and 6 deletions
@@ -59,20 +59,22 @@ object ImuPostureClassifier {
val accMag = sqrt(ax * ax + ay * ay + az * az)
val gyroMag = sqrt(gx * gx + gy * gy + gz * gz)
// Pitch 는 acc gate 통과 여부와 무관하게 항상 계산 (walking detector 가 사용).
val pitchPre = (atan(ax / sqrt(ay * ay + az * az)) * 180.0 / Math.PI).toFloat()
if (accMag < ACC_MAG_MIN || accMag > ACC_MAG_MAX) {
return Result(Posture.UNKNOWN, 0f, accMag, gyroMag,
return Result(Posture.UNKNOWN, pitchPre, accMag, gyroMag,
"acc mag out of range (%.2f)".format(accMag))
}
// Gyro gate 제거 — bias 큰 디바이스에서 영구 fail 문제 (위 주석 참조)
val pitch = (atan(ax / sqrt(ay * ay + az * az)) * 180.0 / Math.PI).toFloat()
val absPitch = abs(pitch)
val absPitch = abs(pitchPre)
val thr = GreenZoneConstants.pitchLyingThreshold
// Pitch 의미: AX 가 중력과 일치할수록 큰 값 (≈90°), AZ 가 중력과 일치하면 작은 값 (≈0°).
// 사용자 직관: 센서 평평하게 둠 (AZ up) = LYING, 세움 (AX up) = NON_LYING (upright).
// → 작은 pitch → LYING, 큰 pitch → NON_LYING
val posture = if (absPitch <= thr) Posture.LYING else Posture.NON_LYING
return Result(posture, pitch, accMag, gyroMag, "ok")
return Result(posture, pitchPre, accMag, gyroMag, "ok")
}
}
@@ -725,10 +725,10 @@ fun PiezoMonitoringView(appState: AppState) {
val walkSnap = walkingDetector.update(samples, r.pitchDeg, nowMs)
val isWalkingNow = walkSnap.isWalking
// Composite posture — WALKING 이 NON_LYING 보다 우선
// Composite posture — WALKING 이 UNKNOWN/NON_LYING 보다 우선 (자체 게이트 신뢰)
val composite = when {
r.posture == com.example.medilightv2android.managers.Posture.UNKNOWN -> com.example.medilightv2android.managers.Posture.UNKNOWN
isWalkingNow -> com.example.medilightv2android.managers.Posture.WALKING
r.posture == com.example.medilightv2android.managers.Posture.UNKNOWN -> com.example.medilightv2android.managers.Posture.UNKNOWN
else -> r.posture // LYING or NON_LYING
}