feat(posture): 데모 idle 시 msp(IMU-only) polling — 사용자앱 동기화 + walking 검출 유지

사용자앱 (feature/tab-navigation b1aecfc, e3c21e6) 의 msp 전환을 데모에도 적용.
demo 의 walking detector 유지 위해 폴링 주기 조정.

배경:
- 기존: PiezoMonitoring idle 에서 mtb 1Hz 폴링 → 매 cycle piezo burst 가청음.
- 사용자앱 실측 (VBTFW0120) 으로 rsp: 18B 정상 응답 확정. 데모 ImuPacketCollector
  는 이미 parseRsp 가지고 있어 (이전에 defensive 작성) BleManager 가 자동 트리거.

수정:
- GreenZoneConstants.posturePollIntervalMs: 1000 → 200ms (5Hz)
  walking detector window (1.5초) 안에 7~8 sample 확보 — mtb 의 15Hz 등가에는
  못 미치지만 사람 보행 freq 1.5~2.5Hz 의 Nyquist 5Hz 충분.
- PiezoMonitoringView LaunchedEffect:
  - idle (auto/single scan 비활성) → sendImuQuery() — 무음
  - auto scan 중 → 기존 mtb 의 rim: 가 자동으로 posture 갱신
- imuCollector.onComplete 는 parseRim / parseRsp 둘 다에서 fire 되니 wiring 그대로.

부하 비교:
  mtb 1Hz  ≈ 1200 B/s  (가청음 발생)
  msp 5Hz  ≈   90 B/s  (무음)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 10:09:23 +09:00
parent 64c15a8d46
commit 9d1e0e62ad
2 changed files with 20 additions and 18 deletions
@@ -36,8 +36,13 @@ object GreenZoneConstants {
/** Posture acc magnitude gate max (g). 이 값 초과면 classifier UNKNOWN. */ /** Posture acc magnitude gate max (g). 이 값 초과면 classifier UNKNOWN. */
@Volatile var postureAccMagMax: Float = 1.2f @Volatile var postureAccMagMax: Float = 1.2f
/** Posture polling 간격 (ms). PiezoMonitoring 에서 mtb 보내는 주기. dev 모드에서 조정 가능. */ /**
@Volatile var posturePollIntervalMs: Long = 1000L * Posture polling 간격 (ms). PiezoMonitoring 에서 msp 보내는 주기.
* 6/30 변경: mtb (1Hz × 15sample) → msp (5Hz × 1sample) 로 전환. piezo burst 가청음 제거.
* Walking detector 의 1.5초 window 안에 7-8 sample 확보 위해 200ms 주기.
* dev 모드에서 조정 가능.
*/
@Volatile var posturePollIntervalMs: Long = 200L
// ── Walking detector 임계값 (flowchart 4-2 / 5-2) ───────────────── // ── Walking detector 임계값 (flowchart 4-2 / 5-2) ─────────────────
/** Walking 인정 최소 pitch (deg). |pitch| > 이 값 이라야 walking 후보. */ /** Walking 인정 최소 pitch (deg). |pitch| > 이 값 이라야 walking 후보. */
@@ -719,10 +719,11 @@ fun PiezoMonitoringView(appState: AppState) {
com.example.medilightv2android.services.PostureEventLogger.startSession(context) com.example.medilightv2android.services.PostureEventLogger.startSession(context)
} }
// IMU polling + 자세 분류 (Phase 1) — 10초 주기로 mtb 명령 → rim: 응답 받아 분류. // IMU polling + 자세 분류 — msp 명령 (rsp: 응답, IMU-only).
// msp 가 펌웨어에서 rsp: 응답 안 보내는 듯해 mtb (alignment) 로 우회. // 6/30: 이전엔 mtb (piezo 6ch + IMU) 폴링으로 매 cycle piezo burst 가청음 발생.
// mtb 는 piezo measurement 도 트리거하므로 piezoCollector.skipNextComplete 로 // 사용자앱에서 msp 의 rsp: 응답 정상 (실측 VBTFW0120, 18B = tag+12B sample+crc) 확정 후
// user 의 측정 callback 가 spurious fire 되는 거 차단. // 데모도 동일 적용. WalkingDetector window (1.5초) 확보 위해 폴링 주기 1000→200ms.
// 둘 다 imuCollector.parseRsp/parseRim 가 같은 onComplete 콜백 트리거 → classifier 동일.
LaunchedEffect(isConnected, isDemoMode) { LaunchedEffect(isConnected, isDemoMode) {
Log.d("ImuDbg", "LaunchedEffect start isConnected=$isConnected isDemoMode=$isDemoMode") Log.d("ImuDbg", "LaunchedEffect start isConnected=$isConnected isDemoMode=$isDemoMode")
if (isDemoMode) { if (isDemoMode) {
@@ -780,20 +781,16 @@ fun PiezoMonitoringView(appState: AppState) {
Log.d("ImuDbg", "imuCollector log: $msg") Log.d("ImuDbg", "imuCollector log: $msg")
} }
while (isActive) { while (isActive) {
// Auto Scan 의 cycle 사이 idle 시간에도 polling 가능 — isAutoMeasuring 조건 제외. // msp 는 piezo burst 없는 IMU-only — Auto/Single Scan 중에도 충돌 없이 가능.
// isMeasuring 은 현재 measure() 호출 중인 짧은 시간만 true → 사이 idle 에서 mtb 가능. // 단 isMeasuring (현재 measure() 호출 중) / isSpotInProgress 짧은 시간만 skip
// isSpotInProgress 는 5회 연속 측정이라 분리 (충돌 회피). // (BLE 큐 충돌 방어).
// 이전: Auto Scan 켜두면 isAutoMeasuring=true 영원 유지 → mtb 영원 skip → posture UNKNOWN // Auto Scan 자체는 mtb 응답에 IMU 가 들어있어 자동으로 posture 갱신됨 (parseRim).
if (isConnected && !isMeasuring && !isSpotInProgress) { if (isConnected && !isMeasuring && !isSpotInProgress) {
Log.d("ImuDbg", "→ sendMtb (posture poll)") if (!isAutoMeasuring) {
bleManager.piezoCollector.skipNextComplete = true // idle — msp polling (무음)
val sent = bleManager.sendMtb() bleManager.sendImuQuery()
if (!sent) {
bleManager.piezoCollector.skipNextComplete = false // throttled, no response coming
Log.d("ImuDbg", "mtb throttled — skip this cycle")
} }
} else { // isAutoMeasuring=true 면 mtb 의 rim: 가 알아서 posture 갱신, msp skip
Log.d("ImuDbg", "skip (connected=$isConnected measuring=$isMeasuring spot=$isSpotInProgress)")
} }
kotlinx.coroutines.delay(GreenZoneConstants.posturePollIntervalMs) kotlinx.coroutines.delay(GreenZoneConstants.posturePollIntervalMs)
} }