feat(posture): Phase 1 — flowchart 기반 자세 분류 + 도넛 우상단 아이콘 (demo-final)
allignment flowchart "Posture classification" 알고리즘 적용. Phase 1
범위: 자세 분류 + 화면 표시만 (알람/감지 없음).
1) ImuPostureClassifier.kt 신규
flowchart 단계 그대로:
- Acc magnitude 게이트: 0.8 ≤ |a| ≤ 1.2 (motion/bad reject)
- Gyro magnitude 게이트: |g| ≤ 9.16 dps (회전 reject)
- Pitch = atan(ax / sqrt(ay² + az²)) × 180/π
- 분류:
· |pitch| < 25° → SUPINE
· 25-70° → SITTING
· > 70° → STANDING
- 게이트 실패 시 UNKNOWN + 사유 반환
- 입력: ImuSample 리스트 (한 응답 15 sample 평균)
※ flowchart 의 "|pitch| > 25; supine" 는 typo 로 판단 → < 25 적용
2) PiezoMonitoringView IMU polling 추가
- 5초 주기로 sendImuQuery() → rim: 응답 → classifier
- UNKNOWN 결과 시 직전 자세 유지 (artifact 흔들림 방지)
- demo mode: SUPINE 으로 fix (BLE 없음)
3) 도넛 차트 우상단 자세 아이콘 표시 (Battery 옆)
- SUPINE → Icons.Hotel (파랑)
- SITTING → Icons.AirlineSeatReclineNormal (주황)
- STANDING → Icons.AccessibilityNew (초록)
- UNKNOWN → Icons.HelpOutline (회색)
- 아이콘 + 텍스트 라벨 (Lying/Sitting/Standing/—)
Phase 2 (다음): Settings 토글 + wake-up state machine + 빨간 팝업.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Posture classifier — flowchart `Posture classification [Flow chart]` 기반 (2026-06-22).
|
||||
*
|
||||
* 단계:
|
||||
* 1. Acc magnitude 게이트: 0.8 ≤ |a| ≤ 1.2 → motion/bad sample 거부
|
||||
* 2. Pitch 계산: atan(ax / sqrt(ay² + az²)) × 180/π
|
||||
* 3. Gyro magnitude 게이트: |g| ≤ 9.16 dps → 회전 중 sample 거부
|
||||
* 4. 자세 분류 (|pitch| 기준):
|
||||
* < 25° → SUPINE (누움)
|
||||
* 25-70° → SITTING
|
||||
* > 70° → STANDING
|
||||
*
|
||||
* 입력: ImuSample 15개 (한 응답 = 300ms 분량). 각 sample 의 평균에서 분류.
|
||||
* 출력: Posture (UNKNOWN, SUPINE, SITTING, STANDING).
|
||||
*
|
||||
* flowchart 의 `|pitch| > 25; supine` 는 typo 로 판단 — `< 25` 로 해석.
|
||||
*/
|
||||
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, SUPINE, SITTING, STANDING }
|
||||
|
||||
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
|
||||
|
||||
// 자세 분류 임계값 (deg)
|
||||
private const val PITCH_SUPINE_MAX = 25f
|
||||
private const val PITCH_STANDING_MIN = 70f
|
||||
|
||||
/** 자세 + 진단 정보 (UI/log용). */
|
||||
data class Result(
|
||||
val posture: Posture,
|
||||
val pitchDeg: Float,
|
||||
val accMag: Float,
|
||||
val gyroMagDps: Float,
|
||||
val reason: String, // why UNKNOWN, or "ok"
|
||||
)
|
||||
|
||||
fun classify(samples: List<ImuSample>): Result {
|
||||
if (samples.isEmpty()) {
|
||||
return Result(Posture.UNKNOWN, 0f, 0f, 0f, "no samples")
|
||||
}
|
||||
|
||||
// sample 평균
|
||||
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)
|
||||
|
||||
// 1. Acc magnitude 게이트
|
||||
if (accMag < ACC_MAG_MIN || accMag > ACC_MAG_MAX) {
|
||||
return Result(Posture.UNKNOWN, 0f, accMag, gyroMag,
|
||||
"acc mag out of range (%.2f)".format(accMag))
|
||||
}
|
||||
|
||||
// 3. Gyro magnitude 게이트 (motion 거부)
|
||||
if (gyroMag > GYRO_MAG_MAX_DPS) {
|
||||
return Result(Posture.UNKNOWN, 0f, accMag, gyroMag,
|
||||
"moving (gyro %.1f dps)".format(gyroMag))
|
||||
}
|
||||
|
||||
// 2. Pitch 계산
|
||||
val pitch = (atan(ax / sqrt(ay * ay + az * az)) * 180.0 / Math.PI).toFloat()
|
||||
val absPitch = abs(pitch)
|
||||
|
||||
// 4. 분류
|
||||
val posture = when {
|
||||
absPitch < PITCH_SUPINE_MAX -> Posture.SUPINE
|
||||
absPitch <= PITCH_STANDING_MIN -> Posture.SITTING
|
||||
else -> Posture.STANDING
|
||||
}
|
||||
return Result(posture, pitch, accMag, gyroMag, "ok")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user