/* * 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): 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") } }