From 9a5fa16f942ec96b254dbab07f83100dd87bf0be Mon Sep 17 00:00:00 2001 From: jjangddu Date: Tue, 23 Jun 2026 17:25:58 +0900 Subject: [PATCH] =?UTF-8?q?fix(posture):=20gyro=20gate=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20+=20UNKNOWN=20streak=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: 디바이스를 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 --- .../managers/ImuPostureClassifier.kt | 12 ++++++------ .../ui/views/monitoring/PiezoMonitoringView.kt | 8 +++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/managers/ImuPostureClassifier.kt b/app/src/main/java/com/example/medilightv2android/managers/ImuPostureClassifier.kt index e66b1f2..a7b7f31 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/ImuPostureClassifier.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/ImuPostureClassifier.kt @@ -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) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index 9ad1682..c937c49 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -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")