6bf399c7b9
증상: 로그캣 분석 — 활발히 걷는 중 (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>
81 lines
3.4 KiB
Kotlin
81 lines
3.4 KiB
Kotlin
/*
|
||
* Posture classifier — flowchart `Posture classification` 기반 (2026-06-22).
|
||
*
|
||
* 단순화: 3 state (SUPINE/SITTING/STANDING) → **2 state (LYING / NON_LYING)**.
|
||
* - 낙상 예방 목적상 sitting/standing 구분 불필요. "누웠나, 일어났나" 만 중요.
|
||
*
|
||
* 단계:
|
||
* 1. Acc magnitude 게이트: 0.8 ≤ |a| ≤ 1.2 → motion/bad sample 거부
|
||
* 2. Gyro magnitude 게이트: |g| ≤ 9.16 dps → 회전 중 sample 거부
|
||
* 3. Pitch 계산: atan(ax / sqrt(ay² + az²)) × 180/π
|
||
* 4. 분류 (|pitch| 기준, threshold = GreenZoneConstants.pitchLyingThreshold):
|
||
* > threshold → LYING (누움)
|
||
* ≤ threshold → NON_LYING (앉음/섬)
|
||
* threshold default = 25°, dev 모드 설정에서 변경 가능.
|
||
*/
|
||
package com.example.medilightv2android.managers
|
||
|
||
import com.example.medilightv2android.ble.ImuSample
|
||
import kotlin.math.abs
|
||
import kotlin.math.atan
|
||
import kotlin.math.sqrt
|
||
|
||
enum class Posture { UNKNOWN, LYING, NON_LYING, WALKING }
|
||
|
||
object ImuPostureClassifier {
|
||
|
||
// 게이트 임계값
|
||
private const val ACC_MAG_MIN = 0.8f
|
||
private const val ACC_MAG_MAX = 1.2f
|
||
// Gyro gate 비활성화 — 일부 디바이스의 gyro calibration bias (-25 dps 등) 가
|
||
// flowchart 임계(9.16) 보다 커서 정지 상태에서도 fail → 분류 영구 불가.
|
||
// accel magnitude gate (0.8-1.2g) 가 이미 큰 모션 거름. gyro 게이트 제거.
|
||
// 단 dev 모드 디버그 위해 magnitude 는 계속 계산해 로그/Result 에 노출.
|
||
|
||
/** 자세 + 진단 정보 (UI/log용). */
|
||
data class Result(
|
||
val posture: Posture,
|
||
val pitchDeg: Float,
|
||
val accMag: Float,
|
||
val gyroMagDps: Float,
|
||
val reason: String,
|
||
)
|
||
|
||
fun classify(samples: List<ImuSample>): Result {
|
||
if (samples.isEmpty()) {
|
||
return Result(Posture.UNKNOWN, 0f, 0f, 0f, "no samples")
|
||
}
|
||
|
||
var sumAx = 0f; var sumAy = 0f; var sumAz = 0f
|
||
var sumGx = 0f; var sumGy = 0f; var sumGz = 0f
|
||
for (s in samples) {
|
||
sumAx += s.ax; sumAy += s.ay; sumAz += s.az
|
||
sumGx += s.gx; sumGy += s.gy; sumGz += s.gz
|
||
}
|
||
val n = samples.size
|
||
val ax = sumAx / n; val ay = sumAy / n; val az = sumAz / n
|
||
val gx = sumGx / n; val gy = sumGy / n; val gz = sumGz / n
|
||
|
||
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, pitchPre, accMag, gyroMag,
|
||
"acc mag out of range (%.2f)".format(accMag))
|
||
}
|
||
// Gyro gate 제거 — bias 큰 디바이스에서 영구 fail 문제 (위 주석 참조)
|
||
|
||
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, pitchPre, accMag, gyroMag, "ok")
|
||
}
|
||
}
|