Commit Graph

418 Commits

Author SHA1 Message Date
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 bece7ea285 fix(ui): posture 라벨 '서있음' → '일어남' 복원 (한글)
PiezoMonitoringView 도넛차트 상단 posture chip 문구.
NON_LYING 표시 텍스트를 이전에 사용하던 '일어남' 으로 복원.
영문 (Upright) 은 그대로 유지.

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
dw.jang 55d55225ab feat(algo): 데모 piezophantomtest 4830e7c/5576a59 신 lib 포팅 — 사용자앱 (e44f0ab) 동기화
## Cross-channel 신 3종 (MethodDRunner)
- applyAntTiebreak: 4830e7c 재작성 (dominant-pick 보호 + tie/shallow/deep 3-branch)
- applyNeighborTopValidate: Rule A (FN 복원) + Rule B (trend FP drop/교체) 신규
- applyInwardPost: 최상단 widest post 과확장 교정 신규 (5576a59)
- researchPostInSpan: seed span 재탐색 + wall/raw ratio gate 재검증
- MethodDResult 확장: post/postRefined/postType 를 var 로 (사후 수정 지원)

## BV 신 helpers (PiezoBVEstimator)
- applyLumenInsetOne: lumen_inset_frac=0.15 (Python default) 벽 인셋
- sagittaMm: chord 대비 최대 수직 이탈
- shrinkSiRadius: Kåsa 원 fit + sagitta shrinkage → R_eff
- minorCapHeight, sphericalCapVolume: 신 cap 공식
- estimateBladderVolume cap 로직 교체:
  R_eff 성공 시 → spherical cap (V = π·h²·(3R-h)/3·lr)
  실패 시 → 기존 hemisphere fallback

## 캘러 갱신
- PiezoMonitoringView: MethodD 벽에 lumen inset 적용 후 BV

## 검증 (사용자앱 feature/tab-navigation)
Python 대비 mean |Δ| = 1.27 mL. 회귀 게이트 (BvIsolationTest) 는 사용자앱에만 유지.

Note: 데모 dps 는 이미 1.936 이라 별도 fix 불필요.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 14:18:20 +09:00
dw.jang 889c65b038 feat(alignment): 데모 CENTER_OPTIMIZE phase 추가 — 사용자앱 (a2fc1d9) 동기화
CH0~CH3 chord CV 로 최적 vertical 위치 판정하는 신규 phase.
V1 GREEN 판정 CV=0.15 실증값을 5-phase 에 이식.

- Phase4 enum: CENTER_OPTIMIZE 추가 (CH3_STABILIZE ↔ LR_BALANCE 사이)
- AlignSummary: chords Array<Double?> (angle-corrected mm) 추가
- summarize(): per-channel chord 계산 (post-ant × cos(angle) × DPS)
- CENTER_OPTIMIZE step 로직:
  * CH3 잃음 → CH3_STABILIZE 복귀
  * CH0/1/2 미검출 → MOVE_UP
  * 4채널 검출 + CV ≤ 0.15 (3 연속) → LR_BALANCE
  * CV > 0.15: chord[3] > chord[0] → MOVE_DOWN, else MOVE_UP
- AlignmentConstants: CV_THRESHOLD_STRICT=0.15, LOOSE=0.20,
  CENTER_OPT_CONFIRM_STREAK=3
- PlacementGuideView: V2 step indicator 5-stage → 6-stage

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 09:59:01 +09:00
dw.jang cd3efc5f24 feat(bv): 데모 lr_ratio_override=1.0 default — 사용자앱 (1add642) 동기화
algorithm 팀 최신 표준 (piezophantomtest 4830e7c) 반영.

- GreenZoneConstants.lrRatioOverride: Double? = 1.0 추가 (default 1.0)
- estimateBladderVolume6ch — override 있으면 computeLrRatio 스킵
- Dev panel toggle "lr=1.0 fixed" — ON/OFF 로 fixed vs computed 전환

Phantom 검증 (2533 scans, 150 mL):
- APP (기존): 147.6 mL (-1.6%)  ✓
- Config F (Python NEW lib + lr=1.0): 147.3 mL (-1.8%)  ← 완전 동일

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-01 11:18:57 +09:00
dw.jang 29cda408a4 feat(alignment): demo AlignGuide 5-phase 로 확장 — FINAL_CONFIRM 추가
feature/tab-navigation 의 AlignmentAdvisorV2 를 demo 에 포팅 (packages 만 rename).

기존 4-phase (INITIAL_ACCUM → VERTICAL_CLIMB → CH3_STABILIZE → LR_BALANCE)
→ 5-phase (+ FINAL_CONFIRM).

FINAL_CONFIRM phase:
- LR_BALANCE 에서 |u4-u5| ≤ 8 도달 시 진입
- buf.clear() 후 5 fresh cycle 누적
- imbal 여전히 ≤ 8 유지 → 최종 STOP
- 실패 시 LR_BALANCE 로 복귀
- 목적: sliding window 잔여 효과 제거 후 재검증

추가 항목:
- Phase 별 dynamic window (accumKVertical=10, accumKLateral=5, accumKConfirm=5)
- lostStreakRequired=3 hysteresis (LR_BALANCE 채널 깜빡임 방지)
- msgKey / msgArgs (i18n 지원)

Sensor alignment default:
- 일반 사용자 (V1): 기존 3-step 유지 (변경 없음)
- Clinical alignment session (V2): 5-phase 사용

PlacementGuideView step indicator: 4 → 5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-01 10:10:10 +09:00
dw.jang 30296f6d62 docs(bv): BvMethod 선택 가이드 — 사용자앱 (98aee75) 동기화
phantom QC vs 임상 인체 사용 가이드 + lr floor 추가 금지 주석.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 16:59:49 +09:00
dw.jang a2d111dc5d feat(detection): 데모 METHOD_D dispatch — 사용자앱 (d354a61) 동기화
DetectionMethod.METHOD_D enum 추가 + when 분기 + UI 4-버튼.
detection + BV 둘 다 Method D 가능. appshare Python pipeline 일치.

- enum DetectionMethod: METHOD_C → +METHOD_D
- when (method) METHOD_D 분기: MethodDRunner.detectMultichannel → allWalls
- BV path: detectionIsMethodD 일 때 recompute 생략
- Dev panel Detection toggle 3-button → 4-button (D 보라색)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 15:17:18 +09:00
dw.jang b2546d14ca fix(alignment): LR_BALANCE lost 채널 hysteresis — 사용자앱 (50b8620) 동기화
2026-06-30 실측 분석 (244 cycle alignment session) 결과:
LR_BALANCE phase 170 cycle 중 |u4-u5| 항상 LAT_TOL(8) 이하 (좌우 균형 늘 OK).
하지만 CH1=15%, CH3=12% 가 cycle 마다 노이즈로 깜빡임 → 매번 lost.isNotEmpty()
true → "↑위로" / "↓아래로" 화살표 → 다음 cycle 복구 → STOP 반복.
사용자 체감: "어디로 옮길지 모르겠음".

- lostStreakRequired=3 추가
- 같은 채널이 3 연속 commit 에서 lost 여야 회복 모드 발동
- streak 미달 시 STOP 유지 (noise 로 판단)
- full set 복귀 시 lostStreak 0 리셋

예상 효과: LR_BALANCE 화살표 flap rate 55% → ~5%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 13:47:54 +09:00
dw.jang 886d40333d feat(demo): BV METHOD_D toggle + alignment graph 10-cycle 평균 표시
사용자앱 변경 (e9b5d9a) 을 데모에도 적용 + clinical alignment 그래프 개선.

## BV METHOD_D toggle (사용자앱과 동기화)

- BvMethod.METHOD_D 추가 (FRUSTUM/V41/METHOD_D)
- PiezoMonitoringView: useMethodDBv 분기. METHOD_D 선택 시 MethodDRunner walls,
  그 외엔 legacy analyzer walls. (계산 cost 회피 위해 toggle ON 일 때만 실행)
- Dev panel BV toggle: 3-버튼 (Method D 보라색 0xFF7C3AED)
- ANT_OFFSET verify 로그: 어느 wall source 인지 표시

## Clinical alignment graph 10-cycle 평균 표시

이전: 매 cycle 마다 raw 단일 신호 그래프 갱신 → 알고리즘이 실제 보는 신호
(10-frame 평균) 와 시각 불일치.
수정: V2 활성 시 RollingAligner.lastAvg 노출 → PlacementWaveformChart 가
overrideRaw 로 평균 신호 표시. V1 일 땐 기존 raw 단일 cycle 유지.

- AlignmentAdvisorV2: lastAvg: List<DoubleArray>? 노출 (push 마다 set, reset 시 null)
- PlacementWaveformChart: overrideRaw 파라미터 (null=기존 raw, non-null=avg)
- ChannelGraphItem: V2 일 때 v2Aligner.lastAvg?.getOrNull(ch) 전달

알고리즘 ↔ 그래프 일치. 임상 alignment 평가 시 정확한 신호 시각 가능.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 12:09:39 +09:00
dw.jang 9d1e0e62ad 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>
2026-06-30 10:09:23 +09:00
dw.jang 64c15a8d46 feat(method-d): appshare 02bed02 (4494098 VBTWD201) 완전 포팅 — wall detection 안정성 확보
Python source: piezo-phantom-test commit 02bed02 (= 4494098 vesiscan-test PR).
Kotlin V2 alignment 결과가 Python 과 1:1 일치하도록 포팅 (값·로직 동일).

[1] MethodDParams (config_d.py 매칭)
  - oscfarWin: 5 → 9
    heavy median 강화로 span_e 안정화 → post 벽 검출 일관성
    (CH0 post SD 1.45→0.36, v2 cycle CV ↓)
  - shoulderPromGamma: 1.0 → 1.5
    peak(promGamma=1.5) 와 정렬해 강한 shoulder 가 약한 peak 에 지지 않음
    peak↔shoulder 재분류 토글 제거 (v2 cycle CV 12.8→9.5%)

[2] MethodDResult (detector.py 매칭)
  - 신규 필드: antCandidates: List<Pair<Int, Double>>
    detect() 가 recovery 전 wall_select 후보를 score 내림차순으로 노출
    Python: tuple((int(c[0]), float(c[4])) for c in sorted(ant_res['candidates'], key=-x[4]))
  - ant / antRefined / urineLen 을 var 로 (tie-break post-process 가 in-place 갱신)

[3] AlignmentConstants (config_6ch.py 매칭)
  - ANT_TIEBREAK_RATIO = 1.3
  - ANT_NEARFIELD_TOL_MM = 8.0

[4] MethodDRunner.applyAntTiebreak() (runners.py _apply_ant_tiebreak 매칭)
  - cross-channel 전벽 보정 (in-place):
    (1) 동률 (top1/top2 < 1.3) → 이웃 center 채널 ant z 합의 median 으로 선택
    (2) near-field 아티팩트 (cz - 8mm 보다 얕음) → 압승이어도 후보 교체
  - 방향성: urine 쪽(깊은) 이탈은 dome 곡률로 정상이라 보존, near-field 만 기각
  - detectMultichannel() 끝에 호출 — Python method_d() 와 동일 순서
  - z(ch, idx) = (DELAY_OFFSET_MM + idx * DISTANCE_PER_SAMPLE) * cos(angle_ch)
  - median 은 numpy.median 동작 매칭 (짝수 길이면 두 가운데 평균)

[5] PiezoBVEstimator.distancePerSample (config_6ch.py 매칭)
  - default 1.968 → 1.936 (HW 샘플링레이트 변경, FW VBTFW118+)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 14:52:37 +09:00
dw.jang 11795d1cef revert(alignment): Skip 우상단(top bar) 만 유지 — 하단 Skip 제거
이전 commit (9a6a81c) 의 BottomCenter floating Skip 롤백.
Skip 진입점은 top bar 우측 TextButton 하나만 (단일 위치).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 12:02:39 +09:00
dw.jang 9a6a81c9f4 fix(alignment): Skip 버튼 중복 제거 + Box.BottomCenter 고정 (V2 등에서 잘리는 문제 fix)
문제:
- top bar 우측 Skip + 하단 scrollable Skip → 중복
- 일반 사용자/clinical 모드는 Column 이 scroll 비활성 (weight 레이아웃) →
  V2 같이 콘텐츠 많을 때 맨 아래 Skip 이 화면 밖으로 잘려 안 보임

수정:
- top bar Skip 제거 (1개만 유지)
- 하단 Skip 을 scrollable Column 밖, Box.align(BottomCenter) 으로 분리 →
  콘텐츠 길이와 무관하게 항상 화면 하단에 노출

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 11:59:43 +09:00
dw.jang 5f2a1b6329 revert(alignment): 일반 사용자 flow 는 V1, V2 는 clinical alignment session 전용으로 복귀
이전 commit (22ef657) 에서 V2 를 모든 진입점 default 로 통일했었으나 — V2 (Method D 4-stage)
는 R&D / clinical alignment session 의 정밀 측정 알고리즘이고, 일반 사용자 (Home → Start →
Personalization → PlacementGuide) 에는 검증된 V1 simple algorithm 이 적합.

복원:
- GreenZoneConstants.alignmentAlgo default V2 → V1
- PlacementGuideView LaunchedEffect: isClinicalAlign ? V2 : V1 (이전 동작)

Step indicator 는 V2 일 때만 4-stage, V1 일 때는 기존 3-stage 표시.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 11:54:48 +09:00
dw.jang 6cff741370 feat(locale): DeviceScanView 한글화 — 기기 연결 페이지
이전 sweep 에서 누락됐던 두번째 '기기 연결' 화면 풀 한글화.

추가 strings: 25 신규 (각 언어)
- '기기 연결' 제목, 검색/탭 안내
- 페어링 필요 알림 (다이얼로그 + 에러 메시지)
- Bluetooth off / 권한 필요 경고
- 검색 중 / 기기 없음 / 트러블슈팅 4단계
- 저장된 기기 / 새 기기 섹션
- 권한 허용 / 검색 시작·중지 버튼
- 최근, 페어링 삭제, 신호 좋음/보통/약함

설계:
- signalLabel(rssi) → 영어 — dev/log 용 유지
- signalLabelRes(rssi) 신규 — UI 용 Locale-aware 라벨

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 11:31:50 +09:00
dw.jang 3b37c816fc fix(demo): 정렬모드 Skip top bar 우측 노출 + 개인화 페이지 한글화
1. PlacementGuide top bar 에 Skip TextButton 추가
   - 기존: scrollable Column 맨 아래 → 스크롤 안에 묻혀 안 보이던 문제
   - 변경: top bar 우측 (Back / Ready / Skip / [dev] Settings 순) 항상 가시
   - 동작: clinical alignment → endMeasurement + CLINICAL_HOME / 일반 → placementComplete

2. PiezoPersonalizationView (개인화 페이지) 한글화
   - 'Maximum Bladder Capacity' → '최대 방광 용량'
   - 'Voiding Reminder' → '배뇨 알림 기준'
   - 'Auto Scan Interval' → '자동 측정 주기' (기존 key 재사용)
   - 'Continue' → '계속' (기존 calibration_continue 재사용)
   - 'Back' (contentDescription) → '뒤로'

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 11:23:11 +09:00
dw.jang 59d4ac0e81 feat(locale): Phase 3 — 전수 sweep, 노인 사용자 기준 영어 잔여 텍스트 한글화
이전 Phase 2 에서 'already English-only' 라며 skip 했던 화면들 — 노인 사용자에게는 영어로
보이면 안 됨. 전체 다시 훑어 모든 user-visible 영어 hardcoded text 를 strings.xml 로 추출.

11 파일 수정:
- MainActivity (Exit dialog)
- HomeView (Smart Bladder Monitoring / Start / Connected / Clinical Mode / dev toast)
- OnboardingView (Welcome, Get Started 등)
- CalibrationView (모든 status + Retry/Skip/Continue)
- SensorSelectView (title, descriptions)
- ShuffledKeypad (Delete contentDescription)
- MeasurementHistoryView (title, tabs, empty state, Today/Yesterday)
- VivamyoMonitoringView (전체 status)
- PiezoMonitoringView (사용자 영역 — Auto/Single Scan, Voiding/Catheterization, 측정 카드, dialog 등)
- PlacementGuideView (V1 hints, Step indicator, Start/Skip/Complete Alignment, In position/Position lost)

추출 strings: ~170 (각 언어). strings.xml 194 → 365 라인.

skip 유지:
- ClinicalHomeView / ClinicalLiveView (R&D dev mode)
- PiezoMonitoring dev panel sliders (3-tap 활성화 dev 도구)
- PlacementGuide dev settings overlay + Echo Waveforms (isDevMode gated)
- BLE Debug Log / Scan Log dialog (dev 전용)
- 브랜드명 (VesiScan-Basic, Vivamyo, Bladdy)
- DEMO 배지, IMU debug 문자열, Log.d / debugLogger 출력

Korean coverage: ~95% (일반 사용자가 절대 보지 않는 dev surface 만 영어 유지).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 09:17:23 +09:00
dw.jang 2cf4f4db2f feat(locale): Phase 2 — 모든 사용자 노출 Korean 문자열 → strings.xml 추출 + 영문 번역
Sub-agent 작업 결과:
- values/strings.xml + values-ko/strings.xml 각각 71 → 194 줄 (약 85 신규 string)
- 17 파일 수정 (UI views 9개, components 3개, managers 1개, services 2개)

상세:
- ui/views/{home, consent, registration, reminder, monitoring (3), personalization, connection}
- components/{BatteryWarningBanner, FirmwareWarningBanner, LockableSlider}
- managers/AlignmentAdvisorV2 — Out/AdvisorState 에 msgKey + msgArgs 추가 (UI 가 context.getString 으로 localize)
- services/{ReminderService, BleForegroundService}

의도적으로 skip:
- ClinicalHomeView / ClinicalLiveView (R&D-only dev mode)
- PiezoMonitoring dev panel labels (dev 도구)
- AlignmentAdvice / 2-phase AlignGuide (UI 미사용)
- 이미 영어인 파일들 (Onboarding, Pin, SensorSelect, Calibration 등)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 17:27:05 +09:00
dw.jang bc7843809e feat(locale): Phase 1 — 언어 전환 인프라 (시스템/한국어/English)
신규:
- LocalePref.kt — Mode {SYSTEM, KO, EN} 영속화 + Context wrap
- MainActivity.attachBaseContext — 사용자 선택 locale 으로 Configuration override
- HomeView 우상단 🌐 Language 아이콘 → 선택 다이얼로그 → recreate

Phase 2: 모든 hardcoded Korean text → strings.xml + values-ko/strings.xml 추출 (대량 작업, 별도 commit)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 16:53:02 +09:00
dw.jang 22ef6576f8 feat(demo): V2 default + 그래프/문구 일치 + Slider lock (스크롤 충돌 방지)
1. V2 alignment 를 모든 진입점 default 로
   - GreenZoneConstants.alignmentAlgo = V2
   - PlacementGuideView LaunchedEffect 의 clinical 한정 분기 제거 → 항상 V2
   - 사용자 경험 통일 (4-stage Method D)

2. 그래프 ↔ 문구 일치 — 잔여 모순 fix
   - CH3_STABILIZE 의 STOP action: 이전엔 isLocked=true + GREEN LED + checkmark
     아이콘 → '완료처럼 보이는데 알고리즘은 stabilizing 중' 모순. 변경: phase가
     LR_BALANCE 일 때만 finalStop → 진짜 GREEN. CH3_STABILIZE STOP 은 아이콘 X.
   - PROBE_LR action: 이전엔 arrow.left (한쪽 화살표). msg 의 ↔ 와 불일치.
     변경: 아이콘 없음 (msg 의 ↔ 만으로 양방향 의미 전달).

3. LockableSlider 신규 — 스크롤 충돌 차단
   - 잠금 아이콘 1회 탭 → 활성화 → drag 가능 → 다시 탭하면 잠금
   - 잠김 상태에서는 slider disabled → 위아래 스크롤 손가락 끌림에 영향 X
   - 적용: PiezoMonitoring 의 dev panel 17 sliders +
          ReminderSettings 의 2 sliders (배뇨 알림 기간, 기상 알람 기준 시간)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 16:45:49 +09:00
dw.jang 618267d8c0 fix(posture): 연결 중 ?_ 지속되던 두 원인 fix
원인 A — Auto Scan 켜져 있는 동안 mtb polling 영원 skip:
  isAutoMeasuring 가 Auto Scan 종료까지 계속 true → mtb 안 보냄 → IMU rim 응답 없음
  → currentPosture 갱신 멈춤 → UNKNOWN 표시 지속.
변경: polling 조건에서 isAutoMeasuring 제거. !isMeasuring 만 검사 →
      Auto Scan 의 cycle 사이 idle 시간 (isMeasuring=false) 에는 mtb 가능.
      Spot 은 5회 연속이라 isSpotInProgress 유지 (충돌 회피).

원인 B — 다른 화면 다녀와도 imuCollector.onComplete cleanup 없어 stale callback 남음:
변경: DisposableEffect 로 onDispose 시 onComplete = null.
      다시 진입하면 LaunchedEffect 가 재등록.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 15:20:32 +09:00
dw.jang de3a607f96 ux(alignment v2): hint 문구를 사용자 행동 중심으로 — ch 기술용어 제거
V2 algorithm hint 만 수정 (V1 그대로):
- INITIAL_ACCUM: '측정 준비 중'        → '잠시 그대로 두세요'
- VERTICAL_CLIMB: '↑ 위로 (ch3 미검출)' → '↑ 위로 조금씩 올리세요'
- CH3_STABILIZE hold: '✓ ch3 검출! 위치 유지' → '✓ 좋아요! 그 위치 유지'
- CH3_STABILIZE lost: '↑ ch3 다시 잃음 — 위로' → '↑ 살짝 놓침 — 다시 위로'
- LR_BALANCE lost: '↑ chN 잃음 — 위치 회복' → '↑ 위치 살짝 벗어남 — 위로 약간'
- LR_BALANCE done: '■ 정렬 완료 (ch1+ch2..., |Δ|=N)' → '✓ 정렬 완료! 그대로 측정 가능'
- LR_BALANCE LR: '← chN 유지 + 좌우 균형 (|Δ|=N)' → '← 왼쪽으로 살짝' (or 오른쪽)
- LR_BALANCE probe: '↔ ch... 좌우 천천히 (lateral 미검출)' → '↔ 좌우로 천천히 움직여 보세요'

PlacementGuideView 의 V2-only baseline 메시지:
- waitingForStart V2 branch: '센서를 치골 위에 두고 Start 를 누르세요'

원본 ch/Δ/setLabel 같은 임상 메타는 dev debug log (ALIGN_V2 ...) 로만 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 11:20:54 +09:00
dw.jang 8f895b354a fix(labdb): cycleToRecord 에 alignment metadata 보존
이전: rowIndex/datetime/commandType/sensor/channels 만 → align_phase 등 drop
변경: align_phase / align_score / align_hint / align_icon / align_detected /
      align_urine_lens 도 record 에 포함 (alignment session 일 때만)

→ labdb 서버 raw.json export 에 cycle 별 V2 phase 표기 (INITIAL_ACCUM /
   VERTICAL_CLIMB / CH3_STABILIZE / LR_BALANCE / STEP_0_BASELINE) 가 들어가
   분석 PC 에서 phase 별 frame 분석 가능.

⚠ 이미 업로드된 sessions 는 phase 없음 (재업로드 필요).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 10:44:02 +09:00
dw.jang 122d9631f3 feat(alignment): Option A — V2 활성 시 graph detection 도 method_d 결과 사용 + 2-column 그래프 + Y축 1.5×
Option A:
- V2 활성 시 channelDetected 를 method_d 의 summary.detected 로 덮어씀
- PiezoEchoAnalyzer (wall pair 양쪽 필요) ↔ method_d (low-echo span만) 의 결과 차이 해소
- ch3 같은 한쪽 벽 약한 채널이 "메시지=검출 / 그래프=미검출" 이던 시각 모순 해결

UI:
- 6개 echo waveform 세로 적층 → 2-column (좌: ch0/1/2/3 머리→치골, 우: ch4/5 좌·우 lateral)
- Y축 그래프 높이 80dp → 120dp (1.5×) 로 peak 가시성 ↑

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 10:37:34 +09:00
dw.jang 12a9089102 feat(clinical): labdb 자동 업로드 제거 — Upload 버튼으로 수동만
이전: endMeasurement → labdb 자동 업로드 → 사용자가 Upload 버튼 (Clinical Home) 못 봄
변경: endMeasurement 는 measurement.json 저장만, 업로드는 Upload 버튼으로만 트리거
→ 사용자가 측정 결과 확인 후 의도적으로 업로드

이미 존재하던 Clinical Home 'Last saved' 카드의 Upload 버튼이 항상 노출됨
(labdb 등록 + 업로드 안 된 상태일 때).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 10:12:18 +09:00
dw.jang 48e6d72270 tune(walking): wmean default 15→10 — 더 짧은 보행도 잡음
이전: 15 (stand-up settling motion 5~10 dps 차단용)
변경: 10 (보행 50+ dps 와 충분히 분리되면서 짧은 보행도 잡음)

GreenZoneConstants.walkingStartWmean + PostureTuningPrefs.D_WALK_START_WMEAN
양쪽 같이 갱신.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 09:26:00 +09:00
dw.jang 37d3d0d84a fix(alignment): clinical session Back/Skip 도 endMeasurement (저장)
이전: Back/Skip → discardAndDeleteFolder (저장 X)
변경: Back/Skip → endMeasurement (Complete 와 동일) — measurement.json + labdb 업로드
→ 사용자가 측정 도중 화면 떠나도 그때까지 누적된 baseline + 4-phase frame 보존

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-25 18:12:54 +09:00
dw.jang 026076aa11 fix(alignment): clinical session — V2 phase label + Step 0 baseline + Back/Skip 분기
#1 placements.jsonl phase 라벨 통일
- 기존: placementPhase.name (V1 phase) 만 저장 → V2 활성이어도 V2 phase 안 보임
- 변경: V2 활성이면 v2Aligner.phase.name 사용 (INITIAL_ACCUM 등 4-stage)
- measurement.json 의 align_phase 와도 동일 라벨로 통일

#2 Step 0 baseline 데이터 캡쳐
- Clinical alignment session 진입 시 isContinuousMode 자동 ON
  (waitingForStart 는 true 유지 — 사용자 Start 누를 때까지)
- baseline cycles 의 phase = "STEP_0_BASELINE" 로 라벨링
- V2 algorithm push 는 !waitingForStart 일 때만 호출 — silent phase 진행 차단
- Start 누를 때 v2Aligner.reset() 추가 (fresh 시작 보장)

#3 Back / Skip 버튼 분기 — clinical alignment session 인 경우 CLINICAL_HOME 으로
- 기존: Back → PIEZO_PERSONALIZATION, Skip → PIEZO_MONITORING (잘못된 화면 이동)
- 변경: clinical alignment session 진입 상태면 discardAndDeleteFolder + CLINICAL_HOME
  (Back/Skip = 저장 의도 X — Complete Alignment 버튼만 저장)
- 일반 placement guide 동작은 그대로 유지

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-25 18:10:40 +09:00