fix(posture): gyro gate 제거 + UNKNOWN streak 화면 표시
증상: 디바이스를 lying 상태로 둬도 "Upright" 표시가 안 바뀜.
진단 (raw 로그 분석):
- 디바이스 정지 상태에서도 gyro y 축 ≈ -25 dps 일관 표시
- 이는 IMU 칩의 **calibration bias** (정지 시 zero offset 안 됨)
- flowchart 의 gyro gate (9.16 dps) 가 항상 fail → classifier 영구 UNKNOWN
- currentPosture 가 UNKNOWN 시 업데이트 안 되는 로직 때문에 첫 valid
값 (NON_LYING, 사용자가 손에 들고 진입한 순간) 으로 stuck
- 사용자가 눕혀도 분류 자체가 안 되니 "Upright" 그대로
수정:
1) ImuPostureClassifier: gyro gate 제거
- accel magnitude gate (0.8-1.2g) 가 큰 모션은 이미 거름
- gyro magnitude 는 Result/log 에 계속 노출 (디버그용)
- flowchart 그대로 따르려면 gyro 도 활용해야 하지만, bias 큰
디바이스에선 무용지물이라 실용성 우선
2) PiezoMonitoringView: UNKNOWN streak 카운터
- 3회 연속 UNKNOWN 이면 currentPosture = UNKNOWN 으로 강제 표시
- silent stuck 상태 (잘못된 자세 계속 보여줌) 방지
- 잠시 1-2 cycle 동안 motion artifact 로 UNKNOWN 떠도 직전
자세 유지 (이전 동작 보존)
3) Log: unknownStreak 도 출력 → 디버그 시 stuck 케이스 가시화
테스트:
- lying → upright 전환 시 즉시 "Upright" 표시되어야 함
- 정지 시 "Lying" 또는 "Upright" 안정적 유지
- 큰 모션 (들어올림 등) 중에는 3 cycle 후 "—" 표시
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -24,10 +24,13 @@ enum class Posture { UNKNOWN, LYING, NON_LYING }
|
||||
|
||||
object ImuPostureClassifier {
|
||||
|
||||
// 게이트 임계값 — flowchart 그대로
|
||||
// 게이트 임계값
|
||||
private const val ACC_MAG_MIN = 0.8f
|
||||
private const val ACC_MAG_MAX = 1.2f
|
||||
private const val GYRO_MAG_MAX_DPS = 9.16f
|
||||
// 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(
|
||||
@@ -60,10 +63,7 @@ object ImuPostureClassifier {
|
||||
return Result(Posture.UNKNOWN, 0f, accMag, gyroMag,
|
||||
"acc mag out of range (%.2f)".format(accMag))
|
||||
}
|
||||
if (gyroMag > GYRO_MAG_MAX_DPS) {
|
||||
return Result(Posture.UNKNOWN, 0f, accMag, gyroMag,
|
||||
"moving (gyro %.1f dps)".format(gyroMag))
|
||||
}
|
||||
// Gyro gate 제거 — bias 큰 디바이스에서 영구 fail 문제 (위 주석 참조)
|
||||
|
||||
val pitch = (atan(ax / sqrt(ay * ay + az * az)) * 180.0 / Math.PI).toFloat()
|
||||
val absPitch = abs(pitch)
|
||||
|
||||
+7
-1
@@ -665,13 +665,19 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
currentPosture = com.example.medilightv2android.managers.Posture.LYING
|
||||
return@LaunchedEffect
|
||||
}
|
||||
var unknownStreak = 0
|
||||
bleManager.imuCollector.onComplete = { samples ->
|
||||
val r = com.example.medilightv2android.managers.ImuPostureClassifier.classify(samples)
|
||||
if (r.posture != com.example.medilightv2android.managers.Posture.UNKNOWN) {
|
||||
currentPosture = r.posture
|
||||
wakeupDetector.update(r.posture)
|
||||
unknownStreak = 0
|
||||
} else {
|
||||
unknownStreak++
|
||||
// 3회 연속 UNKNOWN 이면 화면에 표시 (silent stuck 방지)
|
||||
if (unknownStreak >= 3) currentPosture = com.example.medilightv2android.managers.Posture.UNKNOWN
|
||||
}
|
||||
Log.d("ImuDbg", "POSTURE ${r.posture} pitch=${"%.1f".format(r.pitchDeg)}° accMag=${"%.2f".format(r.accMag)}g gyro=${"%.1f".format(r.gyroMagDps)}dps (${r.reason}) samples=${samples.size}")
|
||||
Log.d("ImuDbg", "POSTURE ${r.posture} pitch=${"%.1f".format(r.pitchDeg)}° accMag=${"%.2f".format(r.accMag)}g gyro=${"%.1f".format(r.gyroMagDps)}dps (${r.reason}) samples=${samples.size} unknownStreak=$unknownStreak")
|
||||
}
|
||||
bleManager.imuCollector.onLog = { msg ->
|
||||
Log.d("ImuDbg", "imuCollector log: $msg")
|
||||
|
||||
Reference in New Issue
Block a user