Commit Graph

25 Commits

Author SHA1 Message Date
dw.jang fcdce12503 fix(alignment): V3 buildRecord BV 리스트 필터 제거 — Python 1:1
Python `position_records:151` 는 `if bv is not None: bvs.append(float(bv.volume_ml))`
으로 nan/0 필터 없이 그대로 추가. nan 발생 시 std/mean 모두 nan → bv_cv=nan
이 되어 `selectCm` tie 정렬에서 +inf 로 취급 (뒷순위).

기존 Kotlin 은 `if (volumeMl.isFinite() && volumeMl > 0)` 필터가 있어 nan/0
케이스에서 Python 과 다른 bv_cv 산출. cm=0 (BV 발산) 처럼 일부 trace 만
이상값일 때 값 오차 발생. Rule A 최종 선택 결과는 동일하지만 line-by-line
대조에서 유일하게 차이나는 지점이라 Python 과 완전 일치하도록 제거.

BV null (검출 실패) 은 여전히 skip (Python 도 동일).
mean/std 결과가 nan/inf 이면 bvCv=null 로 정규화 (Python nan 표현 = Kotlin null).
2026-07-15 14:47:27 +09:00
dw.jang 9387552e18 feat(alignment): V3 CenterAligner 2-Pass 이식 (piezophantomtest #35 BVCV)
Python piezophantomtest ffd436b (2026-07-15 PR #35 'alignment bvcv 규칙 추가')
의 `vesiscan_test.alignment.CenterAligner` 를 Kotlin 으로 이식.

기존 V2 (실시간 sliding window 6-stage) 와 병행. V3 는 Clinical batch 전용:

Pass 1 (scanSelect):
  - 5 위치 (0/1/2/3/4 cm) × 20 cycle → sliding window 10 → 11 trace 씩
  - mean-scan 으로 base 검출 (apply_cross=false) → nch, ch3 판정
  - per-trace cross 검출 → BV 계산 → std/mean = bv_cv
  - Rule A (ch3 필수 + per-trace 검출률 ≥ CH3_HIT_MIN 0.80 + max nch + min bv_cv tie)

Pass 2 (guide):
  - 재측정 한 위치 → target 기준 (ch3='O' & 검출률≥80% & nch≥t.nch &
    bv_cv ≤ t.bv_cv × (1 + bvCvTol)) 충족 시 STOP, 아니면 MOVE_UP.

포팅 원칙:
  - MethodDRunner (기존) 재사용, apply_cross=false/true 두 모드 활용.
  - PiezoBVEstimator.estimateBladderVolume6ch (기존) 재사용.
  - alignment_selection.py 의 select_cm(rule='A', tie='bvcv') 로직 완전 이식.

검증 (CenterAlignerValidationTest, data123 인체 3 세션):
  - Python reference 와 nch/ch3/ch3_hit 완전 일치.
  - bv_cv 오차 <2% (정상 case). BV 발산 케이스 (ch3=X) 만 큰 오차 —
    Rule A 에서 어차피 탈락하므로 최종 선택 영향 없음.
  - 최종 선택 위치 = cm=3 (Python 과 일치).

주의: test 실행 전 PiezoHW.activePreset 를 V1 로 명시 설정 필요.
      실기기는 BleManager 가 device name 으로 autoDetectPreset 처리.

UI 통합 (Pass 1 위치 안내 + Pass 2 판정 화면) 은 후속 작업.
2026-07-15 12:13:34 +09:00
dw.jang 43ad32ba8a refactor(ui): Design Token 중앙화 + AlertModalDialog 컴포넌트화 (Step 1~3)
초 고수 앱 디자이너 코드 리뷰 결과 반영. 저비용/저리스크 항목만 우선 진행,
대형 파일 분해 등은 별도 로드맵.

Step 1: Design Token (Color.kt + Theme.kt)
  - Color.kt 에 semantic token 추가: MlCritical / MlWarning / MlSuccess /
    MlInfo / MlNeutral / MlAccentPurple + ContainerAlpha / ChipAlpha 상수
  - Theme.kt LightColorScheme 를 Purple40 → MlPrimary 로 override.
    기존에는 Material3 default 가 Purple 이라 Button 등 커스텀 UI 와
    색상 불일치 발생. brand 통일.

Step 2: AlertModalDialog 컴포넌트화 (ui/components/AlertModalDialog.kt)
  - 3개 알람 다이얼로그 (Wakeup 주황, Walking 빨강, Detach 주황) 가
    100% 동일 구조 (배경색만 다름) 로 각각 40~50줄 반복.
  - 재사용 가능한 AlertModalDialog(backgroundColor, icon, title,
    subtitle, detail, confirmLabel, onConfirm, widthFraction, iconSize,
    titleSize, dismissible) 로 통합.
  - dismissible=false 가 default (의료 알람은 사용자 명시 확인 필요).
  - PiezoMonitoringView (Wakeup + Walking) / PlacementGuideView (Detach)
    3곳 모두 새 컴포넌트로 교체. 코드량 ~120줄 감소.

Step 3: LabdbAutoRetry 배너 semantic token 적용
  - ClinicalHomeView 자동 업로드 배너의 Color(0xFF1976D2) → MlInfo,
    Color(0xFFFF9800) → MlWarning, Color(0xFF4CAF50) → MlSuccess.
  - ChipAlpha (0.12f) 로 배경 alpha 통일.

효과:
  - 신규 배너/Dialog 는 반드시 semantic token 사용 → 색상 일관성 관리 용이.
  - 색상 변경 시 Color.kt 한 파일만 수정하면 전역 반영.
  - AlertModalDialog 로 신규 알람 추가 시 10~15줄로 가능 (이전 50줄+).

미완료 (별도 로드맵):
  - 기존 파일의 나머지 매직 hex 색상 (~300개) → semantic token 마이그레이션.
    광범위 변경 + 실기 회귀 위험 있어 순차 진행 필요.
  - 대형 파일 분해 (PiezoMonitoringView 2900L, PlacementGuideView 2200L).
  - ViewModel 도입, BLE 콜백 → Flow 이전.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-10 11:52:37 +09:00
dw.jang b822b49fc6 feat(scan): 기기 저장/표시 clinical flow 조건 제거 — 일반 모드에서도 활성
증상:
Clinical mode 로 진입해서 pair 한 기기만 KnownDeviceStore 에 저장되고
DeviceScan 화면 상단의 '저장된 기기' 목록에 표시됨. 일반 모드로 앱을
쓸 때는 매번 fresh scan → 이름 확인 → 연결 반복해야 함.

수정:
- BleManager onConnectionStateChange (STATE_CONNECTED) 안 KnownDeviceStore
  .add() 를 감싸던 `if (inClinicalFlow)` 조건 제거. 연결 성공한 모든
  기기를 영속화.
- DeviceScanView 의 초기 knownDevices 로드도 clinical 조건 제거. 저장된
  기기가 있으면 진입 경로 무관하게 상단에 표시.

효과:
- 일반 사용자도 한 번 연결한 기기를 다음 세션에 목록에서 바로 선택.
- 재연결 대상 자동 감지 (lastConnectedAddress 흐름) 도 clinical 밖에서
  정상 동작.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-10 11:03:14 +09:00
dw.jang db6e7e1a5c feat(labdb): demo-final 도 미업로드 세션 자동 재시도 + endMeasurement 자동 업로드
demo-final 에서도 clinical mode 테스트 중인데 매 세션마다 Upload 버튼을
눌러야 반영되는 문제. tab-navigation / cloud-mvp 브랜치의 자동 재시도
로직을 demo-final 구조에 맞춰 축소 이식.

원인:
- demo-final ClinicalSessionStore.endMeasurement 자체에 자동 업로드
  로직이 아예 없었음 (주석: "labdb 업로드는 자동 X — Upload 버튼으로
  수동 트리거").
- recentFinalizedFolders / capturedCount 히스토리 구조도 없어 단순
  cherry-pick 은 불가.

수정:
- endMeasurement: isRegistered (apiKey 존재) 면 uploadAsync 자동 호출.
- LabdbAutoRetry object 신설 (services/labdb/LabdbAutoRetry.kt):
  demo-final 은 lastFinalizedFolder 단일 값만 있으므로
  listOfNotNull(lastFinalizedFolder) 로 축소. tab-nav/cloud-mvp 는
  recentFinalizedFolders 리스트 사용 (그쪽 브랜치 코드는 그대로).
- ClinicalHomeView LaunchedEffect(Unit) 에서 자동 트리거 + 상단 배너 UI:
  * 진행 중: "자동 업로드 진행 중 · 남은 세션 N · <name>" (파랑, spinner)
  * 완료: "N uploaded, M failed" (초록/주황) + 확인 버튼

효과:
- 세션 종료 후 뒤로가기 → 자동 업로드 시도 → 실패해도 Clinical Home
  재진입 시 재시도. 사용자 개입 0.
- 여러 세션 밀린 상황도 사용자가 다음 세션 진입할 때마다 이전 세션이
  하나씩 재시도됨 (demo-final 구조 특성).
- 기존 폴더 구조 / measurement.json 포맷 100% 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-09 14:09:50 +09:00
dw.jang 990793170e fix(ble): mtb 응답 중 다른 명령 끼어들어 firmware freeze — 3-layer 근본 fix
실측 로그 (2026-07-08 11:18~11:19) 분석: mtb 응답 stream (reb×6 + raa
+ rim, 총 8개 패킷) 중간에 msn (battery) / mim (IMU) 이 TX 로 끼어들면
firmware GATT queue 가 꼬여 응답 실종 → 이어지는 mls mode 0 명령에서
완전 freeze (BLE 광고까지 중단, 재연결 10회 모두 실패).

핵심 원인:
- BleManager 의 batteryTimer / mim polling 이 시간 간격 기반이라 mtb
  응답 진행 중에도 무조건 TX 발사.
- 특히 sendImuFifoQuery 는 imuCollector.reset() 을 먼저 호출 → mtb 의
  rim (IMU) 이 파괴됨.

Layer 1 — BleManager gating
  - isMtbBusy 프로퍼티 추가 (piezoCollector.isMultiChannel && !isComplete)
  - batteryTimer / batteryRetryTimer: sendBatteryQuery 앞에 isMtbBusy skip
  - Watchdog silence heartbeat: sendImuQuery (msp) → sendImuFifoQuery
    (mim) 로 교체 + isMtbBusy skip
  - sendImuQuery 함수 자체 삭제 — msp 명령 코드에서 완전 제거
    (rsp 파서는 legacy 응답용 유지)

Layer 2 — PiezoMonitoringView mim polling gating
  - LaunchedEffect while 루프의 sendImuFifoQuery 앞에 isMtbBusy skip

Layer 3 — mtb 3초 timeout + UI 안내
  - sendMtb: 3초 timeout runnable, raa 응답 오면 취소
  - lastMtbTimeoutAt / consecutiveMtbTimeouts state 노출
  - PlacementGuideView 가 관찰 → 1~2회: "기기 응답 지연" 안내
    3회 연속: "재연결" 안내 + forceDisconnectAndReconnect 자동 호출
  - forceDisconnectAndReconnect 를 public 으로 승격

부가:
- disconnect 3곳 (GATT_ERROR / STATE_DISCONNECTED / watchdog forced)
  에서 mtbTimeoutRunnable 도 함께 정리.

Firmware VBTFW0121 의 mls mode 0 handler 취약점은 별도로 펌웨어 팀에
리포트 필요 (본 fix 는 큐 혼잡 방지로 근본적으로 회피).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 15:03:48 +09:00
dw.jang deac2f50eb fix(ui): "기기 응답 없음" false-positive 배너 — lastBleRxAt 갱신 확대
증상:
posture chip 이 정상 갱신되기 시작한 후에도 도넛차트 상단에 "기기
응답 없음" 빨강 배너가 뜸.

원인:
FirmwareWarningBanner.showTimedOut = timedOut && !deviceAlive.
deviceAlive = lastBleRxAt > 0 && (now - lastBleRxAt) < 12_000.

기존 lastBleRxAt 갱신은 `reb:` (piezo 측정 응답) 케이스에만 있었음.
Auto scan 꺼진 도넛차트 idle 상태에서는 reb 이 아예 안 오고 mim →
rim (IMU) 응답만 매 초 옴. 그래서 lastBleRxAt = 0 유지 → deviceAlive
false → timedOut 되는 순간 배너 노출.

수정:
processReceivedData 진입점에서 무조건 lastBleRxAt = now 갱신. 이제
rim / rsn / rid / rls / rpa / rbb 등 모든 유효 응답이 alive 마크 대상.
Auto scan 상관없이 rim 폴링만으로도 deviceAlive=true 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 09:14:11 +09:00
dw.jang b7aa8f73c2 fix(posture): PlacementGuide / ClinicalLive DisposableEffect 정리도 제거
149e511 이후에도 사용자 실기 테스트에서 posture chip '감지 중' 지속.
Logcat 확인:
- LaunchedEffect start ✅
- rim: 15 samples parsed 매 초 정상 ✅
- POSTURE 로그 여전히 실종

원인: 149e511 은 PiezoMonitoringView 의 DisposableEffect 만 수정했으나
PlacementGuideView (line 173) / ClinicalLiveView (line 131) 에도
`imuCollector.onComplete = null` 이 있어, Compose 재구성 timing 에서
PiezoMonitoring 콜백 세팅 직후 이들의 onDispose 가 실행되면 여전히
null 로 덮임.

수정:
세 곳 모두 imuCollector.onComplete / piezoCollector.onMultiChannelComplete
null 정리를 제거. 다음 화면 진입 시 자기 LaunchedEffect 가 어차피
자기 콜백으로 덮어쓰므로 stale 콜백 실질 무해.
PlacementGuide 의 sendLedMode(0) 는 그대로 유지 (LED 실제 하드웨어 조작).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 09:14:11 +09:00
dw.jang cdd8676a1d fix(posture): 도넛차트 posture chip '감지 중' 무한 유지 버그 복구
증상:
도넛차트 페이지 진입 시 posture chip 이 계속 '감지 중' 만 표시.
Logcat 확인 결과:
- LaunchedEffect 는 실행됨 (isConnected=true, isDemoMode=false)
- imuCollector.parseRim 은 매 초 성공 (`rim: 15 samples parsed`)
- 그런데 POSTURE 로그가 하나도 안 뜸 → onComplete 콜백이 null 상태

원인:
DisposableEffect onDispose 안의 `imuCollector.onComplete = null`.
Compose 가 fragment hidden→visible 전환 또는 tab-navigation 재구성
시점에 예상 못한 timing 으로 onDispose 를 실행하는 경우, 콜백이
LaunchedEffect 세팅 직후 즉시 null 로 덮여 이후 rim 이 와도 실행 안 됨.

수정:
DisposableEffect 에서 imuCollector.onComplete / piezoCollector.
onMultiChannelComplete null 정리를 제거. 다음 진입 시 자기 LaunchedEffect
가 어차피 자기 콜백으로 덮어쓰므로 stale 콜백은 실질 무해.
BleManager 콜백 3개 (onConnectionStateChanged 등) 는 좀비 세션 방지
위해 정리 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 09:14:11 +09:00
dw.jang 0ab273e750 fix(bv): auto scan IMU 회귀 복구 — sendChannelsOnly (maa) → sendMtb
증상:
도넛차트 페이지에서 auto scan 실행 시 posture chip 이 갱신 안 됨.
Line 843 주석 "Auto Scan 자체는 mtb 응답에 IMU 가 들어있어 자동으로
posture 갱신됨" + line 845 조건 `if (!isAutoMeasuring)` 로 auto 중
mim 폴링 skip — 즉 코드는 mtb 를 쓴다는 전제로 짜여 있음.

그러나 실제 measure() 는 `bleManager.sendChannelsOnly()` (=maa) 를
호출. maa 응답은 6ch piezo 만 (rim 없음) → imuCollector 는 채워지지
않음 → posture 완전 실종. 명백한 회귀.

수정:
sendChannelsOnly (maa) → sendMtb (mtb). mtb 응답 = reb×6 + raa + rim
이라 piezoCollector + imuCollector 둘 다 자동 처리. 기존 mim polling
skip 로직 그대로 유효 (auto 중에는 mtb 로 IMU 옴).

동일 로직: PlacementGuideView / ClinicalLiveView 는 이미 sendMtb 사용.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 09:14:11 +09:00
dw.jang 2172304e50 fix(align): GREEN zone 조기 리셋 완화 + 문구 개선
증상:
1. Sensor Alignment 완료 GREEN 진입 후 일정 시간 (약 7초 hold 종료 +
   추가 몇 초) 지나면 UI 가 갑자기 "단계 0/3 / 정렬 시작" 으로
   초기화되어 처음부터 재시작해야 하는 불쾌한 UX.
2. GREEN 문구 "제 위치입니다!" 가 다소 어색.

원인:
PlacementGuideView V1 로직 (line 1098) 의 3-strike 규칙 —
`greenHeld` (7초 hold) 종료 후 `guideResult.isPass=false` 가 **3회
연속** 나오면 전체 리셋 (isLocked=false, waitingForStart=true,
scanCount=0, phase=VERTICAL, rTracker.reset() ...). 그런데 사용자가
GREEN 상태에서 버튼 안 누르고 그대로 기다리면 자연스러운 미세 자세
흔들림 (1~2초) 만으로 3회 실패 성립 → 완전 리셋.

수정:
1. `placement_in_position` "제 위치입니다!" → "최적의 위치입니다!"
   (영문: "In position!" → "Optimal position!").
2. 3-strike → 10-strike 완화. 10회 연속 실패 (약 5초+) 는 되어야
   진짜 이탈로 간주. 그 사이는 GREEN 유지 + "최적의 위치입니다" 표시.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 09:14:11 +09:00
dw.jang 04607241ac fix(ble): 좀비 세션 재연결 실측 대응 — 콜백 duplicate 제거 + watchdog 25초
실측 로그 (2026-07-07 10:43~10:44 세션) 재분석 결과 두 이슈 발견.

1) Duplicate startBatteryPolling
이전 fix (f4f5407) 로 onConnectionStateChanged(true) 가 CCCD write 완료
시점 (BleManager onDescriptorWrite 안 line 1059 startBatteryPolling
직전) 으로 이동. 그런데 PiezoMonitoringView 콜백도 startBatteryPolling
을 호출해 첫 msn battery TX 가 2번 발생 (10:43:46.008 + 10:43:46.040).
Idempotent 라 실제 부작용은 낮지만 노이즈. PiezoMonitoringView 콜백에서
startBatteryPolling 제거 — BleManager 가 이미 담당.

2) Watchdog 25초 (from 15초)
세션 1: 첫 연결 후 CCCD/MTU/CONN_PRIORITY/startBatteryPolling 모두 성공,
그런데 15초간 RX 0개 → forced reconnect. 세션 2: 재연결 후 14초 만에
첫 rim RX 도착 (10:44:00.475) → 스스로 회복. 즉 peripheral / OS BLE
스택의 좀비 회복 시간이 실측 14초. 기존 15초 timeout 은 회복 직전에
reconnect 발동 → 무한 재연결 루프. 25초로 연장하면 대부분 회복 케이스
살릴 수 있음.

세션 1 의 첫 handshake 응답 실종 (mid → rid 응답 없음) 은 앱 코드로
근본 해결 불가 (peripheral 또는 OS BLE 스택 문제). Watchdog 연장은
실질적 완화책.
2026-07-08 09:14:11 +09:00
dw.jang a10d4b9517 fix(stability): 코드베이스 진단 HIGH 이슈 6건 fix
병렬 진단 (BLE/thread + Compose lifecycle + null safety) 결과 HIGH
심각도 12건 중 crash 유발 가능성 높고 저비용 고효과인 것 우선 처리.

**H4 — coroutine leak**
- PiezoMonitoringView.kt BleDebugPanel: `while(true) { delay/refresh }` →
  `while(isActive)`. LaunchedEffect 취소 후에도 refreshTick++ 이 계속
  돌아 GC 방해.
- BladdyRiveView.kt 2곳: 동일 패턴 → isActive 로 통일.

**H1 — BleManager postDelayed 미취소**
- `fwFallbackTimer` (4초 mfv? fallback) + `cccdRetryTimer` (500ms CCCD
  재시도) 를 Runnable 참조로 저장. disconnect / GATT_ERROR / watchdog
  timeout 3곳에서 handler.removeCallbacks 로 명시적 취소.
- 이전에는 disconnect 후에도 4초 후 sendFirmwareVersionQuery() 가 발동
  → 이미 close 된 GATT 에 write → silent exception → state 오염.

**H2 — MeasurementService 콜백 leak**
- performNirsMeasurement/performPiezoMeasurement 는 singleton 에서
  콜백 대입만 하고 정리 안 함. Self-clearing lambda 로 응답 1회 처리
  후 자동 null. 시작 시 이전 stale 콜백도 clear.
- stop() 에서도 대기 중 콜백 취소.

**H3 — Watchdog race condition**
- watchdog thread 가 GATT disconnect/close + characteristic null 을
  binder thread 에서 직접 실행 → UI thread 의 sendRaw 와 race.
- 정리 전부를 handler.post 로 UI thread 에 위임 → single-threaded.
- fwFallback/cccdRetry timer 도 여기서 함께 취소.

**H6 — PiezoMonitoringView measure() closure leak**
- DisposableEffect 에 piezoCollector.onMultiChannelComplete 정리 추가.
  measure() 함수 안에서 이 콜백에 measureScope + channels + outer
  state 를 다수 capture → 화면 이탈 후에도 GC 방해 + 재진입 시 stale
  closure 가 새 상태 오염 위험.

**H9 — UrineCameraScreen NPE 위험**
- LaunchedEffect 안 while 루프에서 `currentDetection!!` 이 다른 recompose
  가 detection 을 null 로 만들면 NPE. Local val snapshot 으로 fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-07 10:40:21 +09:00
dw.jang f4f5407894 fix(ble): 화면 전환 후 좀비 BLE 세션 방지 — 콜백 순서 + 정리
증상: 앱 실행 후 alignment → 도넛차트 → 개발자모드 → alignment 이동 시
BLE 가 끊긴 것처럼 응답 실종 → 15초 후 watchdog forced reconnect.
심하면 재연결도 좀비 상태로 나와 3번째 재연결에서야 정상 동작.

원인 1: BleManager.onConnectionStateChange 에서 STATE_CONNECTED 직후
onConnectionStateChanged?.invoke(true) 호출 → 이 시점에는 아직 MTU/
service discovery/CCCD write 이 완료되지 않아 txCharacteristic 은 null.
상위 (PiezoMonitoringView) 가 startBatteryPolling 등 TX 를 시도하면
sendRaw 내부 null-check 에 걸려 silent drop → peripheral 이 응답을
보내지 않음 → watchdog timeout.

원인 2: PiezoMonitoringView / PlacementGuideView 의 DisposableEffect
에서 BleManager 콜백 (onConnectionStateChanged, onUnexpectedDisconnect,
onReconnectionFailed, piezoCollector.onMultiChannelComplete 등) 을
정리하지 않음. 화면 전환 시 이전 화면의 stale 콜백이 남아 새 화면 진입
후에도 발동 → 상태 충돌.

수정:
- BleManager: onConnectionStateChanged(true) 호출을 CCCD write 완료
  (isServiceReady=true) 시점으로 이동. CCCD 없음 / RX 없음 edge case 도
  동일하게 처리.
- PiezoMonitoringView / PlacementGuideView: DisposableEffect 에서 등록한
  BleManager 콜백 전부 null 로 정리.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-07 10:10:57 +09:00
dw.jang 159116dd94 feat(align): V2 GREEN 진입 후 5초 hold — 순간 flick 으로 즉시 풀림 방지
V1 은 이미 greenHoldMs=7000 로 hold 로직 있으나 V2 (v2Aligner) 는
매 프레임마다 finalStop 을 재판정하고 false 되는 순간 즉시 unlock 되어
"측정 시작" 버튼이 disable 되는 UX 문제. 사용자가 GREEN 뜬 순간
버튼 누를 시간이 부족.

수정: V2 쪽에도 5초 hold 추가. 진입 후 5초 이내에는 finalStop 이
false 로 바뀌어도 isLocked=true, LED mode 6 (GREEN) 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-07 09:36:36 +09:00
dw.jang 49aab7b175 fix(align): 6단계 완료 후 "측정 시작" 버튼 활성화 — FINAL_CONFIRM 인식
기존 finalStop 조건이 `phase == LR_BALANCE` 만 인정했는데, 실제로는
LR_BALANCE 에서 imbal 통과 즉시 `phase = FINAL_CONFIRM` 으로 바뀌어
반환되므로 이 조건에 걸리지 않았음. FINAL_CONFIRM 5프레임 도달 후
STOP + align_complete 반환 시점에도 phase 는 FINAL_CONFIRM 이라
finalStop=false → isLocked 유지 안 됨 → 버튼 disabled.

수정: state=="commit" && phase==FINAL_CONFIRM && action==STOP 을
진짜 정렬 완료로 인정. Accum 중은 이미 앞 branch 로 빠지므로 안전.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-07 09:24:33 +09:00
dw.jang e808b5bc57 fix(align): CH3 flicker 무한 대기 방지 — sliding window majority + relaxed mode
인체 데이터 (data123/) 분석 결과 CH3 (-13.66° down-tilt) 검출률이
Phantom 95.2% → Human 31.8% 로 급락하며 YYNNYY flick 패턴 관측.
기존 "3연속 hit" 진입 조건이 무한 대기에 빠져 사용자에게
"↑ 위로 조금씩" 문구만 반복 노출되는 문제를 해결.

- VERTICAL_CLIMB 진입: 3연속 → 최근 6프레임 중 3회 (majority)
- CH3_STABILIZE / CENTER_OPTIMIZE lost 판정: 4프레임 중 3회 (majority)
- Stuck detection (20프레임 ≈ 5초 대기 시): 상단 3채널 relaxed mode
  진입 → CH3 없이도 정렬 완료 진행
- Soft hint (12프레임 ≈ 3초 후): "CH3 확인 중 · 위치 유지/미세 조정"
  으로 문구 완화 (사용자 답답함 완화)
- AdvisorState.relaxedMode 플래그 추가 (후속 저장 로직 대비)

Python replay (VBT26050202_0CM 22프레임) 로 검증:
- LEGACY: VERTICAL 무한 대기 → 정렬 실패
- NEW: soft-hint → threshold 도달 시 relaxed → 3프레임 만에 LR_BAL 도달

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 17:45:14 +09:00
dw.jang 45f5efc278 fix(alignment): CENTER_OPTIMIZE 방향 오류 — CH0 놓칠 때 MOVE_UP 은 반대
CENTER_OPTIMIZE 단계에서 CH0 만 놓친 상태로 "↑ 조금 더 위로" 안내가 계속 나옴.
Probe 를 더 올릴수록 CH0 beam 이 방광 dome 위로 더 벗어남 → 무한 상승 loop.

CH3 를 잃었을 때도 마찬가지: MOVE_UP 으로 회복 시도하지만 CH3 잃은 원인이 이미
probe 가 너무 올라간 것이라 상황 악화.

V1 preset 기하:
- CH0 +6.89° (위로 tilt, 상단 channel) — 방광 dome 담당
- CH3 −13.66° (아래로 tilt, 하단 channel) — 방광 inferior edge 담당

Probe 위치 vs 미검출 채널:
- probe 너무 낮음 → CH3 miss, CH0 catch → MOVE_UP 정답
- probe 너무 높음 → CH0 miss, CH3 catch → MOVE_DOWN 정답

기존 코드는 CH0~2 미검출을 모두 "너무 낮음" 으로 가정 → MOVE_UP.

CENTER_OPTIMIZE 미검출 처리 방향 결정 로직:
- CH3 잃음 (CENTER_OPTIMIZE 진입 후) = probe 가 위로 올라감 → MOVE_DOWN 으로 회복
- 상단만 미검출 (CH0 or CH0+CH1) = probe too high → MOVE_DOWN
- 하단 포함 미검출 (CH2/CH3 포함) = probe too low → MOVE_UP (기존 default)

- 정상 서있음 자세로 배꼽 근처에서 시작 → CENTER_OPTIMIZE 진입 시 "↓ 조금 아래로" 안내
- 치골 근처에서 시작 → 이전과 동일하게 "↑ 조금 더 위로"
- CH3 검출 후 위로 살짝 지나쳤을 때 "↓ ch3 재확인" 나오는지

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 15:15:58 +09:00
dw.jang 15db5ec8bf feat(algo): 데모 piezophantomtest ea3f15c/b252de0 이식 — post peak-only + apply_cross flag
## 이식 대상
알고리즘팀 main : 6229825 → ea3f15c (fast-forward)
- b252de0 : post wall 후보는 peak-only 로 탐색 (shoulder 제외)
- 4afda38 : method_d(apply_cross) 옵션 추가

## MethodDWallSelect 변경
1. BOUNDARY_PEAK_MARGIN = 2 (post 전용)
   - post 탐색창 hi 를 2 샘플 밖으로 넓혀 view 확보 → 경계에 걸린 peak 이
     shoulder 로 오분류되던 케이스 해결. 수용 범위 [lo, hi] 유지 → farther
     peak 새로 안 받음 (d_max 전역 확대 부작용 회피).
2. side=POST 는 peak-only
   - shoulder(d2 변곡) 는 후벽을 깊은쪽으로 과확장 → BV 과대 (특히 30° 채널).
   - ant 는 shoulder 유지 (near-field 전벽은 shoulder 로 잡히는 게 정상).

## MethodDRunner 변경
- detectMultichannel(applyCross: Boolean = true) 파라미터 추가.
  false 면 교차채널 3종 (ant_tiebreak / neighbor_top_validate / inward_post) 건너뜀.

## AlignmentAdvisorV2 (RollingAligner) 변경
- 정렬 위치 선택은 applyCross=false 로 base 검출 사용.
  이유: 교차채널 보정이 nch 를 바꿔 위치 선택이 흔들리는 것을 차단.
  BV 산출은 PiezoMonitoringView 경로에서 별도로 applyCross=true 로 진행.

## 컴파일
:app:compileDevDebugKotlin / :compileDemoDebugKotlin / UnitTest 모두 통과.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 10:22:56 +09:00
dw.jang 097d086bce tune(walking): walkingStartHoldMs 6000 → 4000ms — UX 반응성
- 걷기 시작 후 4초에 chip "걷는중" 전이 (기존 6초)
- 이중 안전장치 유지:
  * walkingStartWmean=10dps 로 stand-up motion (5~10dps) 이미 필터
  * walkingMinPitchDeg=45° 로 upright 자세만 candidate
- Stand-up 자체가 4초 이내 대개 안정화되므로 false positive 여지 낮음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 11:19:32 +09:00
dw.jang edece0ed14 fix(posture): 걷기 초반 감지중 flash 제거 — UNKNOWN 시 tilt fallback
## 증상
걷기 시작 → 몇 초간 chip "감지중" 유지 → 그 후 "걷는중" 으로 전이.

## 원인
1. 걷기 발 임팩트로 순간 acc mag > 1.2g → classifier acc gate 실패 → UNKNOWN
2. Composite = UNKNOWN → 3연속 후 chip "감지중" 로 넘어감
3. Walking detector 는 6초 hold 진행 중이라 아직 IDLE
4. 6초 뒤 walking 발화 → chip "걷는중"

즉 6초 walking build-up 내내 chip "감지중" 이 뜨고 갑자기 "걷는중" 으로 점프.

## 수정
Composite 로직에서 UNKNOWN 발생 시 tilt(=r.pitchDeg) 로 fallback 판정.
- tilt > 25° (pitchLyingThreshold) → NON_LYING (upright 로 판정)
- tilt ≤ 25° → LYING

이유: tilt 는 acc gate 와 무관하게 항상 계산됨. 걷는 중 avg tilt 는 유효
(각 걸음의 upright 자세 반영). 정확한 pitch 는 어렵지만 lying vs upright
정도는 확신 가능.

## 결과 (예상)
걷기 시작 → chip 즉시 "일어서 있음" (NON_LYING) → 6초 walking hold 완료
후 "걷는중" 으로 전이. 감지중 flash 없음.

정지 자세 + 실제 오류 (acc mag 극단값 etc) 는 여전히 tilt 로 커버되므로
chip 은 최소한 LYING 또는 NON_LYING 을 표시. "감지중" 은 실질적으로 초기
1-2 cycle (buffer 부족) 에만 등장.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 11:13:23 +09:00
dw.jang cd8b48a811 fix(posture): pitch → tilt(Z축) 방향 독립적 분류 — 손 테스트 시 Y축 세로 문제 해결
## 문제
"5초 누워있다가 세워서 5초 움직였는데 다시 누움이 뜸"
= 세워 놓은 5초 동안 NON_LYING 이 뜨지 않음.

## 원인
기존 pitch = atan(ax / √(ay² + az²))
→ AX 축이 세로일 때만 큰 pitch, NON_LYING 인식.
→ AY 축이 세로면 (손으로 기기를 세로로 잡는 흔한 자세) pitch=0 → LYING.

정상 착용 (배 위 부착) 시엔 AX가 세로가 되도록 설계됐지만,
손 테스트에선 AY 축 세로가 되기 쉬움. 특히 흔들기 테스트할 때.

## 수정
tilt = atan(√(ax² + ay²) / |az|)
→ **Z축이 중력방향에서 얼마나 벗어났는지** 측정. 방향 독립적.
- 기기 평평 (AZ up)         → tilt=0°       → LYING     ✓
- X축 세로 (AX up)         → tilt=90°     → NON_LYING ✓
- Y축 세로 (AY up)         → tilt=90°     → NON_LYING ✓ ★ 신규
- 완전 옆으로 세워짐 (az≈0) → tilt=90°     → NON_LYING ✓

## 임계값
`pitchLyingThreshold = 25f` 유지 (Dev 슬라이더 그대로).
- 이전 의미: X축 tilt ≤ 25° = LYING
- 신규 의미: Z축 tilt ≤ 25° = LYING (기기 평평 여유 ±25°)

정상 착용에선 동일하게 동작. hand-test 시나리오 추가 커버.

## 다음
Walking detector 는 pitch 를 그대로 재활용. tilt=90° 근처면
walkingMinPitchDeg=45° 통과. 이전엔 손 세로 잡기로 walking 감지도 못
했지만 이제 가능.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 11:07:41 +09:00
dw.jang 7a2abfa65b fix(ble): 기기 응답 없음 배너 tolerance 5→12초 (흔들림 false positive 차단)
## 문제
기기를 흔들면 "감지중" (posture UNKNOWN) 후 "기기 응답 없음" 배너 flap.

## 원인
FirmwareWarningBanner 의 deviceAlive 기준이 5초로 너무 타이트:
1. 흔들면 BLE 안테나 wobbling → 짧은 disconnect
2. onConnectionStateChange → firmwareVersion.value = "" reset
3. 재연결 후 mid/mfv 재요청 응답 지연 → 8초 후 timedOut=true
4. 동시에 mim? 응답 몇 개 놓쳐 5초 초과 → deviceAlive=false
5. showTimedOut = timedOut && !deviceAlive → 배너 표시

## 수정
deviceAlive 5→12초. Watchdog 자체가 15초 timeout 이라, 12초 내 재개되는
transient 스톨은 배너로 알리지 않고 조용히 대기. 진짜 disconnect 는
watchdog 이 처리.

## 참고
"감지중" (posture UNKNOWN) 은 acc mag > 1.2g 정상 동작 (흔들 때 acc gate
초과 → classifier UNKNOWN). 이미 3연속 hysteresis 있어 순간 튐엔 반응 X.
수정 대상 아님.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 10:57:55 +09:00
dw.jang 506d50d2c6 feat(posture): idle 폴링을 msp → mim 로 전환 — 펌웨어 신규 IMU FIFO 명령 활용
## 배경
6/30 msp 전환으로 piezo 무음은 확보했지만, 1샘플/폴링 이라 WalkingDetector
1.5초 window 를 채우려면 200ms 주기 폴링 필요 → BLE 왕복 5배 트래픽 +
나이퀴스트 aliasing 위험 → walking 감지 실제로 어려움.

7/03 펌웨어 신규 명령 `mim?` 배포:
- 요청: mim? [tag 4B][space 1B][crc 2B] = 7B
- 응답: rim: [tag 4B][total 2B][sample 12B × 15][crc 2B] = 188B
- 15 sample (300ms 시계열) 무음 반환
- 응답 프레임은 mtb? 의 rim: 와 동일 스키마 → 파서 재사용

## 변경
- BleManager.sendImuFifoQuery() 추가 — mim? 명령 전송
  * imuCollector.reset() 선행 (frame buffer 초기화)
  * 기존 sendImuQuery(msp) 는 유지 (watchdog heartbeat 계속 사용)
  * sendRaw 디버그 라벨에 "mim" 추가
- PiezoMonitoringView idle 폴러 sendImuQuery → sendImuFifoQuery
- GreenZoneConstants.posturePollIntervalMs 200 → 1000ms
  * mim 1회에 300ms 시계열 얻으므로 1초 주기로도 walking window 충분히 채움
  * BLE 왕복 5배 감소 → 배터리 유리

## 결과 (예상)
- Idle 도넛 화면에서 walking 감지 정상 작동
- BLE 트래픽·전력 감소
- 응답 파서 (imuCollector.parseRim + onComplete) 그대로 재사용, 로직 무변경

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 10:50:58 +09:00
dw.jang bcbc7b440c refactor: 데모 브랜치 패키지 통일 com.example.medilightv2android → com.medithings.vesiscan
사용자앱 (feature/tab-navigation) 과 패키지 이름 일치. namespace + applicationId
둘 다 com.medithings.vesiscan 로 변경 (기존 데모 앱은 재설치 필요).

## 변경 범위
- Kotlin 148 파일: package + import 문 (714 occurrences)
- 디렉토리 이동: com/example/medilightv2android → com/medithings/vesiscan
  (main, test, androidTest 각각)
- app/build.gradle.kts: namespace, applicationId
- docs/FLAVOR_DEMO_STABLE.md: 참조 갱신
- V41DetectorCH4Test: BvDispatchResult.methodChosen → method (dto field name fix)

## 주의
- applicationId 가 바뀌므로 기존 데모 앱 (com.example.medilightv2android.demo) 은
  Android 관점에서 다른 앱으로 취급 — 재설치 시 PIN/설정 초기화됨.
- Fresh install 권장. 기존 앱 (com.example...) 은 별도로 uninstall 필요.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 14:30:43 +09:00