af469558e6
요구사항: 설정 탭에서 알람 on/off → on 상태에서 일어남 감지 시 빨간 팝업.
변경:
1) ImuPostureClassifier 2-state 단순화
- 기존: SUPINE/SITTING/STANDING (3 state)
- 신규: LYING / NON_LYING (2 state) + UNKNOWN
- sitting/standing 구분 불필요 (낙상 예방엔 "누웠나/일어났나" 만 중요)
- 분류: |pitch| > threshold → LYING, ≤ → NON_LYING
2) GreenZoneConstants.pitchLyingThreshold (default 25°)
- @Volatile var — runtime 변경 가능
- dev 모드 설정에서 slider 로 조정 (10~70°, 11 steps)
3) WakeupAlarmPrefs — SharedPreferences 저장
- isEnabled (default false)
- lyingDurationSec (default 180 = 3분)
4) WakeupDetector — state machine
- LYING 진입 시 timestamp 기록
- LYING → NON_LYING 전이 시 elapsed 측정
- elapsed ≥ lyingDurationSec 이면 onWakeup 콜백
- 1회 발동 후 LYING 복귀까지 재무장 안 됨 (스팸 방지)
- UNKNOWN 은 무시 (motion artifact 흔들림 영향 X)
5) PiezoMonitoringView
- posture 아이콘 매핑 LYING (Hotel, 파랑) / NON_LYING (AccessibilityNew, 초록) / UNKNOWN
- imuCollector.onComplete 콜백에서 wakeupDetector.update(posture) 호출
- showWakeupAlarm Boolean state + 빨간 Dialog (Color 0xFFD32F2F)
· Warning 아이콘 + "환자가 일어났습니다" 메세지
· 누운 시간 표시
· 확인 버튼 (사용자 dismiss 필수, 자동 닫힘 X)
· dismissOnBackPress/ClickOutside = false (실수 dismiss 방지)
- Demo 모드: posture LYING 고정 (BLE 없음)
6) PiezoPersonalizationView — WakeupAlarmCard 신규
- 빨간 톤 카드 + Switch
- "기상 알람 — 환자가 일어나면 빨간 팝업으로 알림 (낙상 예방)"
- 토글 즉시 WakeupAlarmPrefs.setEnabled
7) Dev 설정 (PiezoMonitoringView 의 dev panel)
- "Pitch Threshold" 슬라이더 추가 (10°~70°)
- GreenZoneConstants.pitchLyingThreshold 직접 조정
- 실시간 반영 — 다음 cycle 부터 적용
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
76 lines
2.8 KiB
Kotlin
76 lines
2.8 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 }
|
||
|
||
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
|
||
|
||
/** 자세 + 진단 정보 (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)
|
||
|
||
if (accMag < ACC_MAG_MIN || accMag > ACC_MAG_MAX) {
|
||
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))
|
||
}
|
||
|
||
val pitch = (atan(ax / sqrt(ay * ay + az * az)) * 180.0 / Math.PI).toFloat()
|
||
val absPitch = abs(pitch)
|
||
val thr = GreenZoneConstants.pitchLyingThreshold
|
||
|
||
val posture = if (absPitch > thr) Posture.LYING else Posture.NON_LYING
|
||
return Result(posture, pitch, accMag, gyroMag, "ok")
|
||
}
|
||
}
|