Commit Graph

80 Commits

Author SHA1 Message Date
dw.jang 8536790e6e fix(dev): 배터리 부하 테스트 버튼을 PiezoMonitoring dev 패널로 이동
홈 화면이 아니라 PiezoMonitoring 페이지가 맞는 위치. dev 패널
(개발자 도구 아코디언) 안 Scan Log 버튼 아래로 배치.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 17:12:11 +09:00
dw.jang f19a56694b feat(dev): 기기 배터리 성능 테스트용 임시 부하 버튼 (mim 1s / mbb 10s)
프로브 배터리 소모율 실측을 위해 홈 화면(dev 모드)에 고정 트래픽 부하를
거는 임시 버튼 추가.

- BatteryDrainTester: Handler 기반 루프 소유자. mim? 1초 / mbb? 10초 주기.
  화면 이동·백그라운드와 무관하게 계속 돌고 "정지" 로만 종료.
  mim 은 isMtbBusy 앞에서 skip (imuCollector.reset() 이 mtb/mbb 의 rim 파괴
  방지 · BLE_PROTOCOL_REFERENCE §2.6). 미연결 구간은 송신만 skip 해 auto-
  reconnect 후 부하 자동 재개.
- BatteryDrainTestDialog/Button: 송수신 카운터 · 배터리 전압 delta · 온도 ·
  마지막 응답 경과 · mV/h 소모율 실시간 표시.
- BleManager.onResponseFrame: 계측 전용 수신 프레임 tap 추가 (기존 콜백
  clobber 없이 rim/ric/rbb/rsn 집계). 기능 로직은 이 콜백에 비의존.
- PiezoMonitoringView: 부하 테스트 중이면 자체 mim 폴링 skip (중복 트래픽 방지).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 17:02:55 +09:00
dw.jang 464bbfff06 feat(dev): 앱 종료 시 자동 페어링 해제 (다중 폰×기기 테스트 workflow 자동화)
배경 (사용자 관찰):
  · 폰 A ↔ 기기 A 페어링 후 앱 종료 · 폰 B 로 기기 A 접근 시 실패.
  · 폰 B 에서 "저장된 기기" 삭제해도 기기 A 는 폰 A bond 만 신뢰 · 물리 리셋 필요.
  · 매번 수동으로 Settings > "페어링 해제" 누르기 번거로움 · 잊기 쉬움.

수정:
  1. GreenZoneConstants.autoUnbondOnAppExit: Boolean = false (신규)
     dev-mode 용 flag · 앱 종료 시 자동 unbond 여부.
  2. MainActivity.onDestroy() 훅
     flag ON + 연결된 기기 있으면 → BleManager.disconnectAndUnbond() 자동 실행.
     기기측 msr(unbond+reboot) + 폰측 removeBond · fresh state 유지.
  3. PiezoMonitoringView Settings (dev-mode 조건부)
     "Delete Pairing" 버튼 아래에 토글 카드: "앱 종료 시 자동 페어링 해제"
     설명: "[dev] 다중 폰×기기 테스트용 · 종료 시 unbond"

효과:
  · 폰 A 앱 종료 → 자동 unbond msr 전송 → 기기 A clean
  · 폰 B 접근 시 fresh pairing 즉시 성공
  · 매뉴얼 clean 도 여전히 Delete Pairing 버튼으로 즉시 실행 가능 (기존)

주의:
  · default OFF (일반 사용자 flow 는 bond 유지가 UX 이점)
  · onDestroy 는 앱 강제종료 시 스킵 가능 · 정상 종료 (뒤로가기/스와이프) 는 커버
  · try/catch silent · 종료 시점 실패는 UC-05 (필수기능 아님)

빌드: BUILD SUCCESSFUL 1m 15s.
2026-08-13 11:41:53 +09:00
dw.jang b028060dd5 chore(cleanup): 참조 0 dead code 제거 (Category C 축소 · demo-final)
당초 조사 대상 5개 중 3개 (DfuBleScanner · DfuNus · DfuNusConnection) 는
DfuManager 실제 참조 확인 (컴파일 실패로 발견 · Explore 조사 오탐) → 유지.

완전 제거 (2 항목):
  · speech/HfVolumeExtractor.kt (298 L · 옛 HuggingFace API 실험 · 참조 0 검증)
  · res/raw/bladdy_loading3.riv (asset · 코드 참조 0 · bladdy_final12 만 사용)

okhttp3 dependency 는 LabdbClient (labdb 세션 업로드) 에서 사용 중이라 유지.

합계: 약 298 LOC + asset 1개 감소.
빌드: BUILD SUCCESSFUL.
2026-08-12 15:55:42 +09:00
dw.jang 26dc53b4eb chore(cleanup): YOLO/CameraX/OCR 세트 완전 제거 (Category B · 사용자 요청)
배경: 소변컵 사진 촬영 → YOLO 검출 → MLKit OCR 눈금 인식 기능 폐기.
현재 미사용. Piezo 방광 측정 flow 만 실사용.

완전 제거 (4 파일 + 1 asset):
  · measure/YoloDetector.kt (130 L · ONNX YOLO)
  · measure/SimpleMeasureService.kt (287 L · MLKit OCR + 3-zone)
  · measure/CCPosition.kt (8 L)
  · ui/views/monitoring/UrineCameraScreen.kt (285 L · CameraX 프리뷰)
  · assets/urinecup_best.onnx (~10.6 MB · APK 크기 절감)

Gradle deps 삭제 (build.gradle.kts):
  · com.microsoft.onnxruntime:onnxruntime-android:1.17.0
  · com.google.mlkit:text-recognition:16.0.1
  · androidx.camera:camera-{core,camera2,lifecycle,view}:1.3.4

AndroidManifest.xml 삭제:
  · CAMERA permission
  · android.hardware.camera uses-feature

부분 편집:
  · AppState.kt        — URINE_CAMERA enum value 삭제
  · MainActivity.kt    — import + back-handler branch + Crossfade branch 삭제
  · PiezoMonitoringView.kt — showUrineCamera state · 카메라 카드 (16L) ·
                             LaunchedEffect(showUrineCamera) 삭제
  · strings.xml × 2    — 카메라 전용 문자열 15개 삭제
                        (camera_input 은 CatheterizeSheet 사용 중이라 유지)

측정용 VoidingRecord.kt 는 유지 (CatheterizeSheet · VoidingDiaryView 등에서 사용).
okhttp 는 speech/HfVolumeExtractor 에서 사용 중이라 유지.

합계: 약 780 LOC + 10.6 MB APK 크기 절감.
빌드: BUILD SUCCESSFUL 30s (첫 시도 통과).
2026-08-12 15:49:27 +09:00
dw.jang 1ce19949c9 chore(cleanup): NIRS/VIVAMYO 세트 완전 제거 (Category A · 사용자 요청)
배경: NIRS 는 초기 iOS 앱에서 이식했으나 현재 완전 미사용. Piezo 만 실사용.

완전 제거 (6 파일):
  · managers/NirsManager.kt (94 L)
  · managers/NirsCalcEngine.kt (206 L)
  · models/NirsConstants.kt (49 L)
  · models/NirsData.kt (106 L · NirsMcjPacket · NirsMagResponse · NirsOxyResult)
  · ui/views/monitoring/VivamyoMonitoringView.kt (497 L)
  · ui/views/calibration/CalibrationView.kt (216 L) + calibration/ 디렉토리

부분 편집:
  · AppState.kt      — NirsManager import · NIRS_CALIBRATION/VIVAMYO_MONITORING enum ·
                       calibrationComplete() · sensor 분기 3곳 (goHome/enterDemoMode/
                       deviceConnected/backToMonitoring) 모두 PIEZO 로 단순화
  · MainActivity.kt  — CalibrationView/VivamyoMonitoringView import + Crossfade branch +
                       back-handler branch 삭제
  · BleManager.kt    — 콜백 4개 (onNirsPowerOn/SensorActivated/MagReceived/McjReceived) ·
                       sendRawWrite when-branch 4개 · NIRS Commands 섹션 5 함수 ·
                       RX branch 4개 삭제
  · MeasurementService.kt — performNirsMeasurement 제거 · performMeasurement 단순화 ·
                       stop() 콜백 정리 · SensorMode 유지 (센서 추가 대비)
  · SensorMode.kt    — NIRS enum value 삭제 · PIEZO 만 남음 · UserStorage 는
                       valueOf 실패 시 PIEZO fallback 이라 마이그레이션 불필요
  · SensorSelectView.kt — NIRS SensorCard 제거
  · strings.xml × 2  — sensor_nirs_desc · calibration_* 10개 · vivamyo_* 17개 삭제
                       (calibration_continue 는 PiezoPersonalization 사용 → 유지)

합계: 약 1,300 LOC 감소.
빌드: BUILD SUCCESSFUL 17s.
2026-08-12 15:38:59 +09:00
dw.jang 0fe0687041 ui(dfu): 펌웨어 업데이트 화면 앱 스타일 통일 · 한글화 · 상태칩
미니멈 리디자인 (기존 3카드 구조 유지):
  · 배경 = MlBackground · 카드 = MlCardBackground · RoundedCornerShape 16
  · 헤드라인 "MEDiDFU" 제거 (앱 브랜드와 무관)
  · 카드 label 한글화: "Firmware (bundled)" → "펌웨어 정보" ·
    "Devices (n)" → "발견된 기기 (n)" · "Log" → "상세 로그"
  · Phase 사용자 한글 라벨 + 색상 dot 상태칩:
      IDLE→"대기 중" (회색) · SCANNING→"기기 검색 중…" (주황) ·
      CONNECTED→"연결됨 · 업데이트 준비 완료" (녹색) ·
      UPLOADING→"업데이트 진행 중…" (파랑) · COMPLETED→"완료 ✓" (녹색) ·
      FAILED→"실패" (빨강) · ENTERING_DFU/SEARCHING_DFU 포함
  · 주 액션 "업데이트 시작" = MlTeal→MlPrimary gradient primary 버튼
    (앱 다른 primary 버튼 · Onboarding "Get started" 와 동일 스타일)
  · 보조 액션 (검색/중지/연결 해제/취소/초기화) = outlined + MlPrimary 톤 ·
    취소는 MlCritical 톤
  · 진행률: 8dp 굵은 LinearProgressIndicator · MlPrimary 색 ·
    Row 로 좌 (퍼센트 SemiBold) / 우 (KB / KB 표기)
  · DeviceRow: 이름 SemiBold · address 제거 (RSSI 만) · "DFU 모드" 칩 MlTeal ·
    "Connect" → "연결" · outlined 스타일
  · 권한 안내: 문구 MlSecondaryText · 버튼도 gradient primary
  · 로그: 카드 배경 MlSecondaryText 8% alpha (연한 회색) · 텍스트 MlSecondaryText

Import 추가: MlPrimary/MlTeal/MlBackground/MlCardBackground/MlSecondaryText/
  MlCritical/MlSuccess/MlWarning · ButtonDefaults · Brush · PaddingValues.

빌드: BUILD SUCCESSFUL. (폰 미연결 · install 은 재연결 후 별도.)
2026-08-12 11:30:36 +09:00
dw.jang 6dfe5e6281 fix(ble): disconnect() 시 isConnected.value 즉시 false 반영 (홈 "연결됨" stuck)
증상 (사용자 관찰 2026-08-11):
  · 설정 → Disconnect 눌러 연결 해제 후 goHome() → 홈 화면에 여전히 "● 연결됨"
    chip 표시. 실제 BLE 는 끊긴 상태.

원인:
  · BleManager.disconnect() 가 gatt.disconnect() 만 호출 · isConnected.value 는
    시스템 콜백 (onConnectionStateChange STATE_DISCONNECTED) 이 도착해야 false 로
    갱신됨. 콜백은 비동기 (수 초 지연 가능).
  · 그 사이 goHome() 실행 → HomeView 는 getter (isDemoMode || bleManager.isConnected.value)
    로 값 읽음 · 아직 true → "연결됨" 잘못 표시.
  · PiezoMonitoringView:2385 의 `appState.isDeviceConnected = false` 는 setter 가
    no-op (2026-08-03 fix 이후 getter-only) 라 무효.

수정:
  · disconnect() 첫머리에서 isConnected.value = false 즉시 세팅 (UI 층 즉시 반영).
  · 실제 GATT teardown 은 콜백에서 정상 진행 (idempotent). 재연결 시엔 그때
    콜백으로 다시 true 되므로 로직 훼손 없음.
  · disconnectAndUnbond() 는 이미 postDelayed 안에서 false 세팅 하고 있으나
    이건 사용자 요청 시엔 300ms 정도 지연이 커도 UI 오동작 없음 (즉시 goHome 없음).

빌드: BUILD SUCCESSFUL 20s. installDemoDebug 완료.
2026-08-11 16:08:25 +09:00
dw.jang d1d17f82e7 feat(bv): METHOD_D_PHANTOM 신설 (구 공식) · legacy fork 폐기
브랜치 정책 명확화:
  · demo-final    = phantom 시연 전용 → METHOD_D_PHANTOM (구 가정) default
  · cloud-mvp     = 인체 실사용 → METHOD_D (Halir + adaptive) 유지 · 무영향
  두 브랜치 값이 다른 것이 정상 설계 · phantom vs 인체 방광 기하가 다르기 때문.

METHOD_D_PHANTOM 로직 (estimateBvPhantomSphere):
  · 방광을 완전한 구 (sphere) 로 가정.
  · walls (center CH0~CH3) 의 (post-ant) × dps 로 지름 D 계산 → 채널 평균 D.
  · V = 4/3 · π · (D/2)³.
  · 검증: D = 42 samples × 1.968 dps = 82.7 mm → V ≈ 296 ml (300 ml phantom).

이전 접근 (D-P legacy fork) 폐기:
  · bcbc7b4 시점 PiezoBVEstimator 복사 방식은 여전히 Frustum+cap 조합 · "구 가정"
    이 아니었음. Halir + adaptive 없이 큰 방광 저평가 (150 ml) 문제 발생.
  · legacydp/PiezoBVEstimatorLegacyDP.kt 파일 삭제 · 폴더 제거.
  · BvMethod.METHOD_D_P → METHOD_D_PHANTOM 으로 rename.

파일:
  [삭제] managers/legacydp/PiezoBVEstimatorLegacyDP.kt (821 lines)
  [수정] managers/GreenZoneConstants.kt     — enum + default (METHOD_D_PHANTOM)
  [수정] managers/PiezoBVEstimator.kt       — estimateBvPhantomSphere 함수 신설 (60L)
  [수정] ui/.../PiezoMonitoringView.kt      — dispatcher · useMethodDBv · chip 라벨 "D-Ph"

빌드: BUILD SUCCESSFUL 19s.
2026-08-11 15:55:33 +09:00
dw.jang 748ccbd796 revert(bv): demo-final default → METHOD_D 로 원복 (250ml 재현)
D-P (legacy bcbc7b4) 로직 진단:
  · Halir-Flusser ellipse fit · adaptive_large_bladder_relax · b_si_floor 미탑재.
  · 300ml phantom 시연 값 = 130~150ml (원리상 큰 방광 처리 불가).
  · 사용자 관찰 250ml 는 METHOD_D (Halir + adaptive) 의 결과 · D-P 로는 재현 불가.

조치:
  · default = METHOD_D 복귀. 250ml 스케일 즉시 회복.
  · D-P 는 dev-mode BV chip 에 "D-P" 로 남김 (phantom 소볼륨 실험 · 로직 대조용).
  · Legacy fork 파일 (PiezoBVEstimatorLegacyDP.kt) 유지 · 삭제 안 함.
  · DPS 1.968 은 유지 (사용자 별도 지시).

빌드: BUILD SUCCESSFUL 2s.
2026-08-11 15:14:27 +09:00
dw.jang d054b3b349 tune(bv): default DPS 1.936 → 1.968 (phantom 스케일 재현)
사용자 관찰: 300ml phantom · METHOD_D_P (legacy) 로 130~140ml 밖에 안 나옴.
DPS 스케일 (2026-07 FW 정렬 시 1.968 → 1.936 축소) 이 원인 후보. 볼륨은
distance³ 스케일이라 (1.968/1.936)³ ≈ 1.05 로 5% 정도 회복 · 완전 300 은
아니어도 시연 스케일에 근접.

변경:
  · managers/PiezoBVEstimator.kt:139  _distancePerSample 1.936 → 1.968
  · managers/legacydp/PiezoBVEstimatorLegacyDP.kt:115 동일
  · 두 경로 모두 WdConfig.DPS_DEFAULT 도 동기화 (V41Detector / Geometry /
    AnatomicalGate 가 참조하는 상수).

METHOD_D_P default 는 유지 (사용자 확인).
빌드: BUILD SUCCESSFUL 2s.
2026-08-11 15:08:58 +09:00
dw.jang eb91881873 feat(dev-mode): BV Method 토글에 "D-P" 옵션 추가 + useMethodDBv 라우팅 fix
변경 1 · UI (dev-mode BV chip row):
  · bvMethods 리스트에 METHOD_D_P → "D-P" 추가 (teal 색 #00897B).
  · label 은 공간 절약 위해 "Method D" → "D" 로 단축.
  · demo-final default 인 METHOD_D_P 를 dev-mode 에서 명시적으로 선택/확인 가능.

변경 2 · 라우팅 fix (PiezoMonitoringView:430):
  · useMethodDBv 조건이 BvMethod.METHOD_D 만 확인 → METHOD_D_P 선택 시
    사실상 아무 BV 경로도 안 타는 버그. (이전 커밋 fd7b5da 의 지연 발견 결함).
  · METHOD_D_P 도 동일 경로 (methodDDets 계산 + 아래 브랜치 진입) 통과.
  · dispatcher (line 487~) 에서 실제로 legacydp 로 분기됨.

빌드: BUILD SUCCESSFUL 7s.
2026-08-11 15:00:22 +09:00
dw.jang fd7b5dad53 feat(bv): METHOD_D_P legacy fork · phantom 시연용 (bcbc7b4 시점 로직 격리)
배경:
  · 8/4 default 스위치 (bvMethod V41 → METHOD_D) 순간 · 7/20 도입돼 있던
    Python parity fix 4종 (Halir-Flusser ellipse fit · adaptive_large_bladder_relax ·
    b_si_floor · subsample refined wall) 이 처음 활성화됨.
  · 사용자 관찰 "이전 시연 대비 volume 값이 조금 작음" 은 이 fix 들의 정확도
    개선 (실제값에 가까워짐) 효과. 인체엔 좋으나 phantom (구 형태 방광 모형)
    시연 재현성이 손상됨.

해결:
  · BvMethod 에 METHOD_D_P 추가 (P = phantom · 구 가정) · demo-final default 로.
  · bcbc7b4 (2026-07-02) 시점 PiezoBVEstimator 를 legacydp 서브패키지에 격리.
  · BVResult · WallWithSpan · GreenZoneConstants 등 최상위 shared 심볼은 최신 참조
    (BVResult 신규 optional 필드는 default 로 자동 채워짐).
  · PiezoHW 는 legacy 파일 내부에 격리 (bcbc7b4 시점 V0/V1/V2 preset · 최신
    R100/R200/R300 preset 변경과 무관하게 phantom 재현성 유지).

파일:
  · [신규] managers/legacydp/PiezoBVEstimatorLegacyDP.kt (821 lines)
    - bcbc7b4 원본 855 lines 에서 BVResult 정의 (40 lines) 제거 · import 로 대체
    - package = com.medithings.vesiscan.managers.legacydp
  · [수정] managers/GreenZoneConstants.kt — BvMethod 확장 · demo default 변경
  · [수정] ui/views/monitoring/PiezoMonitoringView.kt — dispatcher 분기
    (WallWithSpan → Pair<Int,Int> 변환 · legacy estimateBladderVolume6ch 호출)

효과:
  · demo-final default = METHOD_D_P → phantom 값 재현
  · 인체용/개발용은 dev-mode 토글로 METHOD_D 선택 가능 (기존 로직 무손상)
  · cloud-mvp 브랜치의 METHOD_D 개선사항은 계속 원본 코드로 흘러올 수 있음
    (legacy 는 완전 격리 · touch 안 됨).

빌드: BUILD SUCCESSFUL 8s (첫 시도 통과).
2026-08-11 14:41:48 +09:00
dw.jang 73e2596842 fix(alignment): "기기 응답이 지연되고 있습니다" stuck + 재진입 stale 카운터
증상 (사용자 로그 2026-08-11 12:35):
  · 첫 alignment 완료 → PiezoMonitoringView → alignment 재진입 시
    "기기 응답이 지연되고 있습니다" 문구가 뜨고 사라지지 않음.
  · 실제 BLE 는 mtb 1회 timeout 후 다음 프레임에 즉시 raa 정상 응답 → 카운터 리셋됐으나
    화면만 못 따라감.

원인 (2가지):
  A. LaunchedEffect(mtbTimeoutCount) 에 mtbTimeoutCount==0 브랜치 없음.
     카운터 1→0 정상 복구 시 문구 초기화 로직이 없어 stuck.
  B. 재진입 시 이전 view (PiezoMonitoring) 에서 남은 mtb timeout 카운터가
     그대로 유지 → 첫 mtb 성공 전 UI 가 이미 "응답 지연" 로 초기화될 수 있음.

수정:
  A) when 절 재구성 · == 0 브랜치에서 delayed/reconnect 문구였다면 clear.
  B) 화면 진입 LaunchedEffect(Unit) 에서 consecutiveMtbTimeouts.value = 0.

RSSI 는 원인 아님 — 재진입 구간 RSSI -65~-62 로 양호.
CMDQ (depth=1) 상 이전 view 잔여 mim IMU 큐 지연이 mtb 1회 timeout 을
유발한 것으로 보이나 · 그건 즉시 자동 복구되므로 UI 층만 손보면 됨.
2026-08-11 13:35:51 +09:00
dw.jang 0a02fc8b87 feat(ui): OnboardingView 첫 화면에 앱 버전 표시 (demo flavor "2.0.0-demo")
사용자 요청 (2026-08-11): 맨 첫 화면 (Onboarding) 에도 버전 노출.
기존엔 HomeView 에만 있음 · 첫 화면 진입 시 확인 어려움.

변경:
  · OnboardingView subtitle 아래에 versionName (12sp · secondary text)
  · packageManager.getPackageInfo().versionName 사용 → demo flavor 는
    자동으로 "-demo" 접미사 반영 → "2.0.0-demo" 표시.
  · runCatching · 실패 시 빈 문자열 (필수기능 영향 없음).
2026-08-11 12:11:44 +09:00
dw.jang b2c1ba2101 fix(alignment): detach 지연 · 힌트 왔다갔다 fix · V1 sanity + Detachment OR 규칙
증상 (사용자 보고 2026-08-11 · demo-final):
  센서가 아예 분리된 상태인데 · alignment 화면에서 전벽/후벽이 하나씩
  잡혀 "위로/아래로" 힌트가 왔다갔다 · 몇 초 기다린 후에야 "분리됨"
  팝업이 뜸.

원인 (두 요인 조합):
  1) DetachmentDetection AND 규칙 (mean<30 AND std<30) 이 노이즈 상황에
     너무 관대. 공기 중 EMI/RF 노이즈로 std 가 30 이상인 경우 흔해
     detach 판정 지연.
  2) Method C detection 이 노이즈 peak 을 우연히 벽으로 오검출 · alignment
     computePlacementGuide 가 그 벽으로 힌트 계산 → 왔다갔다.
  3) V1 default 흐름에는 0/6 walls sanity check 없음 (V2 전용).

수정:
  1) DetachmentDetection: AND → OR
     mean·std 둘 중 하나만 30 미만이면 detach. 노이즈 std 높아도 mean
     낮으면 즉시 판정. 실제 방광 신호는 mean·std 둘 다 100+ 수준이라
     false positive 실질 낮음.

  2) PlacementGuideView V1 브랜치 sanity check 이식 (V2 line 924-949 로직
     동일).
     6채널 모두 wall 미검출 → 부착 확인 안내 · Start 후면 waitingForStart
     복귀 + 팝업 강제 (2026-08-05 V2 sanity 와 동일 UX).

Method C 노이즈 오검출 자체는 별건 · sanity + 강화된 Detachment 로 UX
관점 방어.
2026-08-11 10:44:00 +09:00
dw.jang 1aa15fad1a fix(alignment): detach 상태에서 Start 눌러도 계속 팝업만 뜨는 무한루프 fix
증상 (사용자 보고 2026-08-11 · demo-final):
  Alignment 중 detach 판정 → 팝업 + step 0/3 회귀.
  사용자 재부착 후 Start 버튼 눌러도 "다시 부착하세요" 팝업만 계속.

원인:
  이전 fix (2026-08-04) 는 Start onClick 시 prevDetached=true 면 팝업 재발생 +
  진행 차단. 의도는 "부착 안 하고 Start 눌렀을 때 안내".
  문제: waitingForStart=true 상태에서는 auto-scan (isContinuousMode=false)
  이 멈춰 있어 prevDetached 값이 갱신 안 됨. 사용자가 실제 재부착해도
  prevDetached 는 이전 detach 값이 stale 하게 남아 Start 눌러도 항상 팝업.

수정:
  Start onClick 시 stale 상태 리셋 (prevDetached=false, showDetachAlert=false)
  후 정상 진행. 실제 detach 이면 첫 scan cycle 의 rising edge (line 882)
  로 팝업 자동 재발생 + waitingForStart=true 회귀 → 안내 유지.
  실제 부착이면 정상 진행. auto-scan 이 항상 진짜 상태를 판정.
2026-08-11 10:31:33 +09:00
dw.jang 8cab1d084c change(bv): demo-final default 조합 재조정 · detection=C + bv=METHOD_D
사용자 요청 (2026-08-10 재조정):
  · detectionMethod: METHOD_C (유지 · walls 검출 phantom-검증 파이프라인)
  · bvMethod: V41 → METHOD_D  (BV 계산은 Python parity 적용)

이유:
  - walls 검출은 기존 방식 (Method C) 이 phantom QC 에서 안정
  - BV 계산은 Method D 로 · estimateBv(walls) wrapper 가 lumen_inset(0.15)
    + adaptive_large_bladder_relax + b_si_floor 자동 적용 (Python 1:1)
  - PiezoMonitoringView 의 useMethodDBv 분기가 Method C 검출된 walls 를
    Method D 방식으로 BV 재계산 (기존에 이미 지원)

cloud-mvp 는 detection/bv 둘 다 METHOD_D 유지.
2026-08-10 18:13:37 +09:00
dw.jang 5d64b69282 change(bv): demo-final ROLLBACK · default detectionMethod/bvMethod 8/4 이전으로 복귀
사용자 요청 (2026-08-10): demo-final 버전만 8/4 이전 default 로 롤백.
  · detectionMethod: METHOD_D → METHOD_C (2026-08-04 0184c6d 이식 롤백)
  · bvMethod: METHOD_D → V41         (2026-08-04 f3b15ce 이식 롤백)

METHOD_D 는 dev panel 에서 여전히 수동 선택 가능. cloud-mvp 브랜치는 METHOD_D 유지.

2026-08-04 변경 이유 (Method D 로의 통일 · 그래프 walls vs BV 값 파이프라인 일치)
는 유효하지만 · demo 버전은 시연/안정 검증용으로 기존 (C · V41) 회귀 확인이
필요해 롤백.
2026-08-10 17:50:06 +09:00
dw.jang 99bb904f13 fix(alignment): 0/6 walls 시 즉시 팝업 강제 (rising-edge 놓치는 케이스)
증상 (사용자 보고 2026-08-05):
  6채널 다 안 잡히는데 "센서 분리됨" 팝업이 안 뜸.

원인:
  DetachmentDetection 은 mean·std AND rule (threshold 30) — EMI/노이즈로 std ≥ 30 이면
  isDetached=false 반환 → rising-edge 트리거 (line 882) 가 발화 안 됨. 화면상 6채널이
  전부 미검출이어도 팝업이 안 뜨는 gap.

수정:
  0/6 walls sanity check 에서 showDetachAlert = true 를 직접 세팅.
  prevDetached=true 도 같이 세팅해 재부착 시 auto-dismiss 정상 동작.

이전 fix (982df04) 는 waitingForStart 복귀까지만 처리했음. Start 재클릭 시 팝업이
뜨긴 했지만 사용자 입장에선 여전히 "안 뜬다" 로 느껴졌음.

feature/cloud-mvp d4dac71 과 대칭 (cloud-mvp 는 팝업 UI 자체를 새로 추가).
2026-08-05 17:47:53 +09:00
dw.jang 982df04ca7 fix(alignment): 0/6 walls 감지 시 Start 화면 복귀 (아무것도 안 됨 버그)
증상 (사용자 보고 2026-08-05):
  센서 부착 안 한 상태에서 "정렬 시작" 눌러도 화면상 아무 반응 없음.

원인:
  이전 sanity check 는 hint 만 "부착 확인" 으로 override 하고 waitingForStart
  는 false 로 유지 → Start 버튼 숨겨진 채로 hint 만 반복 · 사용자는 뭘 할지
  몰라 "안 됨" 처럼 인지.

수정:
  0/6 walls 감지 + !waitingForStart 조건일 때:
    - isContinuousMode = false (스캔 중단)
    - waitingForStart = true (Start 버튼 다시 표시)
    - prevDetached = true (Start 재클릭 시 즉시 팝업, 기존 Start 버튼 가드 활용)

  waitingForStart = true 상태 (baseline) 에서는 기존대로 hint 만 override
  (Start 안 눌렀는데 리셋할 필요 없음).

feature/cloud-mvp e71f63b 과 대칭 이식.
2026-08-05 17:23:33 +09:00
dw.jang 41ca909888 fix(alignment): 0/6 walls sanity check · 부착 안 됐을 때 "위로 올려" false positive 방지
증상 (사용자 보고 2026-08-05):
  Alignment 모드에서 센서를 부착하지 않았는데 "↑ 위로 올려" 힌트가 계속 노출.

원인:
  DetachmentDetection 임계 (mean AND std 둘 다 < 30) 를 노이즈로 통과하면
  alignment 알고리즘이 그대로 실행됨. AlignmentAdvice 는 CH3 미검출을
  "misaligned" 로 해석 → MOVE_UP 반환. "detached" 케이스와 구분 못함.

수정 (Option B: sanity check + Option C: 명확 문구):
  PlacementGuideView V2 branch 에서 v2Aligner.push 결과의
    detected.count { it } == 0 (6채널 모두 wall 미검출) 감지 시
  alignment advice 무시하고 placement_check_attachment 힌트로 override.

  strings.xml (KR/EN):
    placement_check_attachment
      · KR: "부착 상태 확인 필요 — 모든 채널에서 신호가 감지되지 않습니다.
             센서를 치골 위에 밀착시켜 주세요."
      · EN: "Attachment check needed — no signal from any channel.
             Ensure sensor is firmly placed above pubic bone."

효과:
  - 노이즈만 있고 실제 부착 없는 경우 "위로 올려" 반복 대신 명확한 부착 안내
  - alignment 알고리즘 자체는 유지 (0/6 만 특별 처리 · 1/6 이상은 정상 흐름)
  - DetachmentDetection 임계는 그대로 (실제 부착·미부착 경계 케이스 영향 없음)

검증: BUILD SUCCESSFUL 17s
2026-08-05 16:25:45 +09:00
dw.jang 13ba88b72b fix(firmware): VBTFW ↔ MCUboot semver 매핑 · update-available 정확 판정 (FW 팀 스펙 2026-08-05)
증상 (사용자 보고):
  기기 fw = VBTFW0200 · 번들 DFU = 1.0.0+5 · 앱이 "새 펌웨어 사용 가능" 배너 노출.
  실제로는 기기 (2.0.0) > 번들 (1.0.0) 이라 업데이트 불필요.

원인:
  기존 isFirmwareUpdateAvailable 은 단순 String contains 비교 (`!fw.contains(bundled)`)
  → "VBTFW0200" 은 "1.0.0+5" 를 포함 안 하므로 무조건 true. 방향성 없음.

FW 팀 스펙 (2026-08-05 슬라이드 · Firmware 식별 코드):
  A. 공식 펌웨어 버전: VBTFW MMNN → M.0.N (major=MM, patch=NN, minor 항상 0)
     · VBTFW0100 = 1.0.0
     · VBTFW0200 = 2.0.0
     · VBTFW0201 = 2.0.1
  B. MCUboot/DFU 이미지: major.minor.patch+build (build 는 major/minor/patch 비교 무관)
  C. 두 포맷 1:1 매칭 관리 (VBTFW0100 = 1.0.0+0)

수정:
  BleManager.parseFwSemver(raw): Triple<Int, Int, Int>?
    - VBTFW / VB0FW / VBFW 정규식 (`VB\d?FW(\d{2})(\d{2})`) → (M, 0, N)
    - MCUboot 정규식 (`(\d+)\.(\d+)\.(\d+)`) → (major, minor, patch)
  BleManager.compareSemver(a, b): Int (튜플 3단계 비교)
  isFirmwareUpdateAvailable: bundled 튜플 > device 튜플 일 때만 true
    → 기기가 이미 최신이거나 동일하면 배너 미노출

검증 케이스:
  device VBTFW0200 · bundled 1.0.0+5 → (1,0,0) < (2,0,0) → false (no banner)
  device VBTFW0100 · bundled 2.0.0+0 → (2,0,0) > (1,0,0) → true (banner)
  device VBTFW0200 · bundled 2.0.1+0 → (2,0,1) > (2,0,0) → true (banner)

빌드 SUCCESSFUL 37s.
2026-08-05 14:04:09 +09:00
dw.jang 4eb67e7450 feat(firmware): 새 DFU zip (1.0.0+5) 번들 + 버전 불일치 시 업데이트 배너
DFU 파일:
  - dfu_application_2+35.zip (구) 삭제
  - dfu_application_1.0.0+5.zip (신규 238KB) 추가

FirmwarePackage:
  - DEFAULT_ASSET = "dfu_application_1.0.0+5.zip"
  - BUNDLED_VERSION = "1.0.0+5" 상수 신규 (manifest.json version_MCUBOOT 와 일치)

BleManager:
  - isFirmwareOutdated: 신규 semver 포맷 ("1.0.0+5") 은 legacy VBTFW build-number
    비교 제외 (false positive 방지 · parseFirmwareBuild 가 tail 숫자 5 를 118 미만
    으로 오판정 안 하도록).
  - isFirmwareUpdateAvailable (신규): 런타임 fw 문자열이 BUNDLED_VERSION 을 포함
    하지 않으면 true. 사용자에게 "새 펌웨어 사용 가능" 배너 노출.

FirmwareWarningBanner:
  - update-available 파랑 배너 신규 (정보성 · 우선순위 outdated > timedOut > update).
  - onUpdateAvailableTap 콜백 · 파랑 배너 탭 시 FIRMWARE_UPDATE 화면 이동.
  - PiezoMonitoringView · PlacementGuideView 호출부에 콜백 전달.

strings.xml (KR/EN):
  - firmware_update_available_title/desc

동작 흐름:
  1. 앱 연결 → mid?/rid: 로 fw 수신 (예: "VBTFW0200")
  2. isFirmwareUpdateAvailable → true (fw !contains "1.0.0+5")
  3. 상단 파랑 배너 "새 펌웨어가 있습니다 · 기기: VBTFW0200 · 신규: 1.0.0+5" 노출
  4. 탭 → FIRMWARE_UPDATE 화면 → DFU 진행 → 성공 시 fw 갱신 후 배너 자동 사라짐

향후 FW 팀이 버전 스키마 확정 시 BUNDLED_VERSION 만 갱신하면 됨.
2026-08-04 17:24:59 +09:00
dw.jang a992a88b86 feat(ble): unbondSmart() · 삭제 시 자동 재연결 + 오프라인 경고 팝업 (cloud-mvp 1982399 이식) 2026-08-04 17:03:38 +09:00
dw.jang 6007c29c9d fix(monitoring): ChannelPanel (dev-mode) METHOD_D 미적용 버그 fix
증상 (사용자 보고 2026-08-04):
  PiezoMonitoring 페이지 · 개발자모드 · 아래 CH0~CH5 차트와 색상(초록/빨강)
  이 Method D 검출과 일치하지 않는다.

원인:
  ChannelPanel (line 2591~) 이 curMethod == METHOD_D 여도 else 분기에서
  legacy `analyzer.analyzeChannel` (V4.1/Method C 스타일) 로 fallback.
  → 실제 BV 계산은 METHOD_D 파이프라인이지만, 화면 표시용 walls 는 legacy 로
     계산되어 시각적 불일치. 사용자가 "D 적용 안됨" 으로 오인.

수정:
  ChannelPanel 두 call site (Zone indicator + 채널 status bar · Per-channel waveform)
  가 공통 helper `analyzeForPanel(chData)` 통과.
  curMethod == METHOD_D 시 MethodDRunner.detectMultichannel 을 6채널 한 번 실행 (재사용).
  결과를 ChannelAnalysisResult (LowEchoResult) 형태로 변환해 legacy caller 와 호환.
  METHOD_A · else (V4.1/METHOD_C) 는 기존 유지.
2026-08-04 16:33:20 +09:00
dw.jang 97e4933871 fix(alignment): demo-final PlacementGuide settings 에 METHOD_D 옵션 추가
증상 (사용자 보고 2026-08-04):
  Alignment 설정 화면 · Detection 셀렉터에 A/B/C 만 있고 D 가 없음.
  Clinical / Alignment 에서 Method D 가 적용 안 된 것처럼 보임.

원인:
  demo-final PlacementGuideView.kt:1323-1327 에 METHOD_D 항목 누락.
  최근 default detectionMethod 를 METHOD_D 로 변경 (`0184c6d`) 했으나 이 셀렉터
  UI 는 여전히 A/B/C 만 노출 · 사용자가 선택 화면 진입 시 어떤 버튼도 highlight
  되지 않아 "적용 안됨" 으로 오인.

  실제로는 default METHOD_D 가 코드에서 정상 적용 중 (BV 계산 · walls 검출 모두).
  UI 만 옵션 4개 중 3개만 보였던 것.

수정:
  methods 리스트에 METHOD_D 추가 · colors 도 4개로 확장 (D=magenta).
  cloud-mvp 는 이미 4개 옵션 노출 중이었음 · demo-final 이 뒤쳐진 상태였음.
2026-08-04 16:22:25 +09:00
dw.jang 253312b4b0 fix(logs): §C+E · purge dead code 활성화 + record() 통합 관문 (cloud-mvp d8425d0 이식) 2026-08-04 16:18:07 +09:00
dw.jang 4d8ada0634 fix(logs): §D · cyclesBuffer 무한 누적 → cycles.jsonl 스트리밍 (Priority 2)
배경 (LOG_STORAGE_ANALYSIS §D):
  ClinicalSessionStore.cyclesBuffer 는 endMeasurement() 호출 전까지 모든 사이클을
  메모리 보관. MeasurementCycle 1개 ≈ 16KB · autoCapture 600ms → 시간당 ~90MB 힙.
  장시간 임상 세션 OOM 위험.

수정:
  cyclesBuffer (MutableList<MeasurementCycle>) 완전 제거 · cycleCounter (Int) 로 대체.
  addCycle / addAlignmentFrame 은 이제:
    1. MeasurementCycle 생성
    2. appendCycleToStream() → cycles.jsonl 에 즉시 append (fire-and-forget)
    3. cycleCounter++

  writeMeasurementJson (endMeasurement 시점):
    - cycles.jsonl 를 useLines 스트리밍 read → cyclesArr 조립
    - measurement.json 생성 성공 시 cycles.jsonl 삭제 (SSOT 유지)
    - 파싱 실패 line 은 skip (corrupt 방지)

  startMeasurement:
    - 이전 세션 잔여 cycles.jsonl 삭제 (같은 folderName 재사용 방지)

  discardAndDeleteFolder / endMeasurement:
    - cycleCounter = 0 리셋

효과:
  메모리 상시: cycleCounter (4B) 만 · 이전 대비 시간당 90MB 절감
  End 시점 read-back 은 필요 (measurement.json aggregate) · 그때만 잠깐 spike
  Cycle 저장 실패해도 측정 계속 (silent · fire-and-forget UC-05)

측정.json 스키마 변경 없음 · guardian app / 분석 툴 하위 호환.

검증: BUILD SUCCESSFUL 2s
2026-08-04 16:00:54 +09:00
dw.jang 1df1a4e6cd fix(logs): §A+B · scan_id 정합성 · events.jsonl 조인키 (LOG_STORAGE_ANALYSIS Priority 1)
배경 (LOG_STORAGE_ANALYSIS 2026-08-04):
  §A · ble.log 의 scan= 값이 실제 adc.csv scan_id 대비 1씩 밀림 (off-by-one)
       - 원인: measurementResult() 가 AdcCsvLogger.lastScanId 를 읽은 후에
         AdcCsvLogger.log() 가 scanCounter 를 증가시켜 실제 CSV 는 N+1 로 기록.
  §B · events.jsonl 에 scan_id 필드 부재 → 4개 로그 (adc.csv · imu.csv · ble.log ·
       events.jsonl) 간 조인 키 없음. timestamp 밀리초만으로는 autoScanIntervalMs=600ms
       에서 신뢰 불가.
  §F · BV 실패 케이스가 events.jsonl 에 아무 흔적 없음.

수정:
  AdcCsvLogger:
    - nextScanId(): Int 신규 · thread-safe counter 증가 후 id 반환
    - log(...): 새 scanId 파라미터 · 호출자가 미리 발급한 id 우선 사용
    - counterLock 으로 동시성 방어

  MeasurementLogService.logMeasurement(...):
    - scanId: Int? 파라미터 추가 · events.jsonl 에 "scan_id" 필드 기록

  BleDebugLogger.measurementResult(...):
    - scanId: Int? 파라미터 · 호출자가 발급한 id 우선 (미제공 시 legacy fallback)

  ImuCsvLogger.log(...):
    - scanId: Int? 파라미터 · 호출자가 발급한 id 우선

  PiezoMonitoringView (5 sites · onMultiChannelComplete):
    - 사이클 진입 시 val scanId = AdcCsvLogger.nextScanId() 미리 발급
    - useV41Bv · useMethodDBv · effectiveCenterWalls>=4 · totalWallCount>0 ·
      no-valid-channels 5경로 모두 scanId 전달
    - §F: BV_SKIP · BV_FAIL 케이스도 events.jsonl 에 기록 (null volume)

  ClinicalLiveView (1 site):
    - autoCapture 시 val sid = AdcCsvLogger.nextScanId() → AdcCsvLogger + ImuCsvLogger
      에 동일 id 전달

효과:
  ble.log · adc.csv · imu.csv · events.jsonl 이 scan_id 로 정합 조인 가능.
  Guardian 앱이 이벤트 재구성 시 timestamp 대신 scan_id 단일 키 사용.
  BV 실패도 audit trail 에 기록.

검증: BUILD SUCCESSFUL 8s
2026-08-04 15:58:01 +09:00
dw.jang 1e3610ca0f fix(alignment): 탈착 상태에서 Start 누를 시 팝업 재발생 + 진행 차단
증상 (사용자 보고 2026-08-04):
  1. 센서 탈착 → "센서 분리" 팝업 뜸 (rising edge · 1회)
  2. 사용자가 팝업 dismiss
  3. 센서 여전히 탈착 상태 · 사용자가 "정렬 시작" 버튼 다시 누름
  4. 팝업 안 뜨고 스텝만 0/3 → 1/3 → 0/3 회귀 · 이유 없이 되돌려짐

원인:
  - Start onClick 은 waitingForStart=false 로 진행만 시킴
  - 다음 scan cycle 이 line 883 (detach 지속) 감지 → waitingForStart=true 재설정
  - showDetachAlert 는 `nowDetached && !prevDetached` rising edge 만 발동 → prev 여전히
    true 라 팝업 재발생 안 됨
  - 결과: 스텝이 침묵 회귀 · 사용자 혼란

수정:
  Start onClick 진입 시 prevDetached=true 이면 즉시 showDetachAlert=true 후 return.
  Start 진행 자체가 차단됨 · 사용자에게 "센서 부착 후 다시 시도" 명확 안내.
2026-08-04 15:24:05 +09:00
dw.jang 96895e5c08 feat(ble): R100 견고성 3종 · startup 지연 대응 + hang 안내 + 재연결 제한
배경 (2026-08-04 · VBT0607R100 로그 분석):
  - 연결 직후 첫 msn/mid 명령이 9초간 무응답 → CMDQ timeout 3회 · 사용자 노출
  - mpa → mtb 발사 후 GATT status=8 (link supervision timeout) · 기기 hang
  - 이후 재연결 5회+ 실패 · 기기가 air 상에 안 잡힘 (advertising 죽음)
  - FW 사이드 문제 · 앱 층에서 UX 완화 가능한 부분 개선

변경:
1. INITIAL_TX_DELAY_MS = 2_000ms — CCCD write 후 첫 명령 2초 지연
   R100 boot 늦은 케이스에서 초기 timeout 반복 방지. Watchdog 은 즉시 시작.

2. GATT status 8/19/22 별도 처리
   - status=8  GATT_CONN_TIMEOUT (link supervision timeout · FW hang)
   - status=19 GATT_CONN_TERMINATE_PEER_USER
   - status=22 GATT_CONN_TERMINATE_LOCAL_HOST
   → connectionError = "DEVICE_UNRESPONSIVE:\$status" marker
   → UI 에서 "전원 15초 롱프레스로 재부팅 후 다시 연결하세요" 안내
   Bond conflict (status=5) 는 기존대로 별도 처리 유지.

3. MAX_RECONNECT_ATTEMPTS 10 → 5
   실패 후 connectionError = "RECONNECT_GAVE_UP" marker + onReconnectionFailed
   → UI 에서 "기기가 켜져 있는지 확인 후 다시 시도" 안내
   무한 시도 로그 스팸 방지.

4. mtb_response_reconnect 문구 개선
   "연결을 재시도합니다" → "재부팅이 필요할 수 있습니다 (전원 15초 롱프레스)"
   3회 consecutive mtb timeout 시 사용자에게 더 actionable 안내.

strings.xml (KR/EN):
   - ble_error_device_unresponsive (재부팅 안내)
   - ble_error_reconnect_gave_up (재연결 포기 안내)
   - mtb_response_reconnect 갱신

FW 팀 조사와 병행. 로그 (2026-08-04_134048) FW 팀 전달 예정.
2026-08-04 14:12:22 +09:00
dw.jang 68577139d4 feat(ble): FW query 자동 재시도 · "기기 응답 없음" 배너 false-positive 감소
배경 (2026-08-04 · 사용자 · Alignment idle 시 배너 flap):
  연결 직후 mid? (t=0) → mfv? fallback (t=4s) 만으로도 응답 없으면
  firmwareVersion 이 empty · 8초 후 FirmwareWarningBanner 의 timedOut=true
  → Alignment idle 처럼 트래픽 적은 화면에서 배너 뜸.

신규 로직 scheduleFirmwareRetry():
  - 5초 간격으로 mid? / mfv? 교차 발사 (최대 3회)
  - 첫 응답 오면 즉시 중단 (firmwareVersion.isNotEmpty())
  - 3회 모두 실패 → 재시도 종료 · 배너는 그때 노출 (진짜 문제)

Timing:
  t=0    mid?              (기존)
  t=4s   mfv? fallback     (기존) · 여기서 scheduleFirmwareRetry() 시작
  t=9s   mid? retry #1     (신규)
  t=14s  mfv? retry #2     (신규)
  t=19s  mid? retry #3     (신규)
  t=24s+ 재시도 종료 · 응답 없으면 배너

Disconnect / GATT error / 좀비 경로에도 stopFirmwareRetry() cleanup.

검증: BUILD SUCCESSFUL 2s
2026-08-04 12:25:12 +09:00
dw.jang 0184c6d694 change(detect): default detectionMethod METHOD_C → METHOD_D (cloud-mvp 1896e9b 이식) 2026-08-04 12:08:25 +09:00
dw.jang f3b15cef63 change(bv): default bvMethod V41 → METHOD_D (cloud-mvp b49170e 이식) 2026-08-04 11:58:03 +09:00
dw.jang 40cf9d224e fix(hw): R100/R200/R300 각도 스펙 갱신 · raw 각도 + lateral 아래10°/좌우5°
배경 (2026-08-04 · 사용자 · HW 스펙 변경):
  실제 피부 입사각 = 아래 방향 10°, 좌우 방향 5°.
  Python HW_VERSIONS 를 raw 각도 그대로 사용하도록 개정 (Snell 후 보정 미적용).
  V1/V2 는 ABS→gel Snell 보정 유지, R 시리즈만 raw.

변경 (degreeAll):
  R100: [0.0, -4.4, -8.7, -13.0, -6.87, -6.87] → [0.0, -7.0, -14.0, -21.0, -10.0, -10.0]
  R200: [3.1, -2.5, -7.5, -13.0, -6.87, -6.87] → [5.0, -4.0, -12.0, -21.0, -10.0, -10.0]
  R300: [0.0, -5.6, -11.2, -16.6, -6.87, -6.87] → [0.0, -9.0, -18.0, -27.0, -10.0, -10.0]

변경 (degreeLRAll · CH4/CH5 lateral):
  R100/R200/R300: [-3.42, +3.42] → [-5.0, +5.0]

sensor_z_mm 은 변경 없음 (V2 와 동일 [19.3, 13.0, 6.7, 0.0, 9.85, 9.85]).

검증: BUILD SUCCESSFUL 2s
2026-08-04 11:20:05 +09:00
dw.jang fda9a4d97f feat(ble): GATT status=5 (FW allowlist bond conflict) UX (cloud-mvp 10c45dc 이식)
FW 팀이 1:1 bond allowlist 정책 추가 예정. 다른 central 과 이미 bond 중인
기기에 붙으면 GATT_INSUFFICIENT_AUTHENTICATION (status=5) 로 거부.

BleManager:
  - bondConflictAddress: MutableStateOf<String?> 신규
  - status=5 감지 시 bondConflictAddress=addr + connectionError="BOND_CONFLICT:$addr"
  - CONNECTED 성공 시 clear

DeviceScanView (ui/views/connection/):
  - 에러 배너 확장 (Column 2단):
    · 안내 문구 (기기 15초 롱프레스로 본딩 삭제)
    · "이 폰의 본딩 정보 초기화" OutlinedButton (unbondAndRemoveAddress)
  - Toast 완료 안내

strings.xml (KR/EN): ble_error_bond_conflict / ble_action_reset_bond / ble_toast_bond_removed
2026-08-04 10:54:41 +09:00
dw.jang de6e1d1931 feat(guardian): Rev2 · Bus 확장 + AlarmAggregator + NotificationSubscriber
GUARDIAN-01 §Rev2 개선 4건 (파트너 앱 개발 지원):

1. BleManager · Battery/BleConnection emit 지점 추가
   - emitBatteryEvent(pct): 임계 bucket 전이 (CRITICAL<5, LOW<20, OK≥20)
   - emitConnectionEvent(state, reason): CONNECTED / DISCONNECTED / FAILED
   - CONNECTED · STATE_DISCONNECTED · GATT_ERROR · 좀비 watchdog 4 경로 wire

2. AlarmAggregator (신규 · telemetry/) — subscriber 참조 구현
   - Bus 구독 · BvEvent/PostureEvent/GaitEvent/BleConn/Battery → AlarmEvent 재발행
   - 재귀 방지 (filterNot { it is AlarmEvent })
   - Category 매핑: BV_URGENT/BV_WARN/POSTURE_TRANSITION/GAIT_STARTED/STOPPED/
     BLE_LOST/BATTERY_CRITICAL/BATTERY_LOW
   - Guardian 앱 로직 단순화 (AlarmEvent 하나만 구독하면 됨)

3. Dev-mode overlay 확장 (PiezoMonitoringView · RSSI 옆)
   - ClinicalEventBus 최근 5개 이벤트 rolling 표시
   - Type(BV/PST/GAI/BLE/BAT/ALM) + severity color + 요약
   - LaunchedEffect + mutableStateListOf · isDevMode gate

4. NotificationSubscriber (신규 · telemetry/) — in-app 소비 참조 구현
   - AlarmEvent 구독 · Android push notification 발행
   - BATTERY_CRITICAL/LOW → device_status 채널 (신규)
   - BLE_LOST → 30초 debounce · CONNECTED 시 reset
   - BV_URGENT/WARN → AppState.updateLevelFromMeasurement 직접 호출과 중복 방지 · NO-OP
   - MainActivity.onCreate 에서 start(this) 1회 호출

NotificationService 확장:
   - device_status 채널 신규 (배터리·BLE 알람용)
   - sendBatteryNotification(level, isCritical) · bucket dedup
   - sendBleLostNotification(reason) · resetBleDebounce()

의도: Guardian 앱 (동업자 · Supabase 경유) 이 in-app subscriber 와 동일 패턴으로
소비하도록 참조. 다음 단계는 GUARDIAN-02 파트너용 스키마 문서 (cloud-mvp).
2026-08-04 09:52:15 +09:00
dw.jang f05068002b feat(ble): CommandQueue · 응답 대기 후 다음 명령 전송 (FW 팀 지시)
배경 (2026-08-04):
  FW 팀 지시 — 이전 명령의 응답 도착 전에 다음 명령 보내지 말 것.
  FW GATT queue 가 얕아 백투백 write 시 silent drop / freeze 유발.
  기존 isMtbBusy gate 는 일부만 방어 (battery/IMU polling ↔ mtb 중간)
  → 모든 명령에 통일된 sequential 큐 도입.

CommandQueue.kt (신규 · 155 line):
  - QueuedCommand(data, label, isDone(tag3), timeoutMs=3000)
  - enqueue → pending==null 이면 즉시 dispatch, 아니면 대기
  - 응답 tag 마다 onResponseTag() → pending.isDone 체크
  - 공통 에러 응답 (rxx/rxd/rxn/rxc/rxs) 은 무조건 완료 처리
  - 3초 timeout → onDrop 콜백 (UI observer 갱신 포함)
  - clear(reason) — disconnect 경로에서 pending + 큐 전체 drop
  - Thread-safe (synchronized on internal lock)

BleManager.kt:
  - sendRaw() → sendRawWrite() private (실제 GATT write)
  - 새 sendRaw() = 단일 응답 (m→r 자동 치환) 명령용 wrapper · enqueue 위임
  - 스트리밍 명령은 자체 send fn 에서 explicit enqueue:
    · maa/mbb → 종료 = raa && piezoCollector.isComplete
    · mtb     → 종료 = (rim|ric|raa) && piezo done && imu samples 채워짐
    · mim     → 종료 = (rim|ric) && imu samples 채워짐
    · mec     → 종료 = raa
  - processReceivedData 말미에 commandQueue.onResponseTag(tag3) hook
  - 공통 에러 응답 5종 (rxx/rxd/rxn/rxc/rxs) case 신설 · 로그
  - disconnect 5 경로에 commandQueue.clear("disconnect") 추가
  - onDrop 콜백에서 mtb timeout 시 UI observer (lastMtbTimeoutAt +
    consecutiveMtbTimeouts) 갱신 · 기존 UX 유지

BLE 스펙 참조: ppt/ble.xlsx (25 명령 · 응답 태그 표).
2026-08-04 09:30:48 +09:00
dw.jang 815774e23c feat(dev): dev-mode RSSI live overlay · PiezoMonitoringView 도넛차트 위
BleManager:
- val rssi: MutableStateOf<Int?> — 미연결 시 null
- startRssiPolling() / stopRssiPolling() — 2초 주기 readRemoteRssi()
- Service ready 3 경로에 startRssiPolling() wire
- disconnect · gatt error · reconnect · forceDisconnect 5 경로에
  stopRssiPolling() + rssi.value = null

PiezoMonitoringView:
- val liveRssi by bleManager.rssi
- Donut 위에 dev-mode gate overlay (badge chip):
  · null → "-- dBm" gray
  · >= -60 dBm → green (strong)
  · >= -75 dBm → amber (ok)
  · <  -75 dBm → red (weak)
- 개발자모드 (3-tap Bladdy) 진입 시에만 표시

일반 사용자 UI 영향 없음 (isDevMode=false 기본).
2026-08-03 18:04:51 +09:00
dw.jang 165df3c63d feat(telemetry): Phase B/C/D · ClinicalEventBus + emit (cloud-mvp 6856a7b 이식)
Phase D · telemetry/ClinicalEvent + ClinicalEventBus 신규 (demo-final path):
- sealed 계층 · schemaVersion · severity · isTransition · anonymized sessionId
- MutableSharedFlow (replay=0, buffer=64, DROP_OLDEST · fire-and-forget)
- PhiRedactor 없음 (demo-final) · TelemetryHash.sessionId (inline SHA-256) 사용

Phase A → BvEvent emit (AppState.updateLevelFromMeasurement):
- 매 측정마다 emit · urgency 전이 시 isTransition=true
- bvMethod 이름 · anonymized sessionId

Phase B/C → PostureEvent/GaitEvent emit (PiezoMonitoringView):
- composite posture 전이 시 PostureEvent (매 사이클 X · 트래픽 절약)
- walkingTransitioned 시 GaitEvent (severity=WARN on WALKING 진입)
- prevCompositePosture 상태 신규 추적

기존 SyncEvents/PostureEventLogger 와 병렬 · 서로 독립.

Refs: GUARDIAN-01 §3-2 이벤트 스키마 · §5 Phase A~D

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 17:22:48 +09:00
dw.jang 432127a667 feat(state): Phase A · 실측 경로 알람/이력 통합 (cloud-mvp deb7b17 이식)
Guardian 공유 이전 선행 작업 (GUARDIAN-01 §5 Phase A).

이전 버그: 데모 경로 (incrementLevel) 만 NotificationService · HistoryStore 호출.
        실측 (BLE) 경로는 bladderLevel = ...copy(currentLevel = level) 로 직접
        대입 → 알람 미발생 · 이력 미저장.

수정:
- AppState.updateLevelFromMeasurement(volumeMl, recordHistory) 신규 함수
  · bladderLevel 계산·갱신·storage 동기화
  · MeasurementRecord 저장 (recordHistory=true 시)
  · Urgency 변화 시 sendUrgencyNotification (중복 억제는 기존 로직 재사용)
- AppState.incrementLevel(): 데모 1단계 증가를 신규 함수로 위임
- PiezoMonitoringView 3곳 (L578/L1345/L1550) 직접 대입 → 신규 함수 호출
  · auto-scan 갱신: recordHistory=false (폭탄 방지)
  · SPOT trimmed 실측: recordHistory=true
  · dev-mode tap: recordHistory=true

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 16:58:06 +09:00
dw.jang 9470ca7dbf feat(hw): PiezoHW R100/R200/R300 preset 3종 추가 (cloud-mvp 4fbba01 이식)
이번에 새로 만든 실리콘 렌즈 타입 목업 3종 (VBT2607R100/R200/R300) 을
demo-final 앱 알고리즘도 인식하도록 DevicePreset 확장.

목업 사양 (Firmware Zephyr · 피부내 초음파 입사각):
  R1: raw 0/7/14/21° → Snell 0/-4.4/-8.7/-13.0°
  R2: raw -5/4/12/21° → Snell 3.1/-2.5/-7.5/-13.0° (CH0 위쪽 +3.1)
  R3: raw 0/9/18/27° → Snell 0/-5.6/-11.2/-16.6° (최대 27°)
lateral CH4/CH5: 아래 10° · 좌우 5°

DevicePreset: LEGACY_5CH · V0/V1/V2 유지 + R100/R200/R300 신규
autoDetectPreset: R\d{3} 패턴 우선 매칭 · fallback 은 옛 naming
각 preset: sensor_z V2 동일 · degree Snell 값 · degree_lr V1/V2 동일

Note: 안테나 변경으로 RSSI 저하 · GATT status=8 발생 관찰됨. HW 이슈 · 앱과
      무관 · Firmware/HW 팀 안테나 튜닝 개선 필요.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 16:28:01 +09:00
dw.jang 638b8e454c fix(firmware): auto-connect 시 Scan UI 조건부 숨김 (cloud-mvp d0e5ca7 이식)
target 설정된 상태 (auto-connect 성공) 에서는 Scan 버튼 · Devices 리스트 숨김.
target 없거나 IDLE/SCANNING phase 에서만 노출.

Fallback: 연결 없이 진입하거나 auto-connect 실패 시 기존 스캔 UI 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 15:44:13 +09:00
dw.jang 33c4e94d07 feat(firmware): 이미 연결된 기기 있으면 스캔 생략 · 바로 재연결 (cloud-mvp 19a1c12 이식)
이전 fix (47317b3) 는 진입 시 disconnect 만 했지만 사용자가 여전히
Scan → Connect 를 수동으로 눌러야 했음. UX 낭비.

이제:
1. 진입 → 이미 연결된 기기 체크 (BleManager.isConnected)
2. BluetoothDevice + name 캡처
3. 메인 앱 disconnect (프로브 광고 재개)
4. 1.5s 대기 (BLE stack 정리)
5. DfuManager.connectDirectly(device, name) — 스캔 생략
6. 사용자는 CONNECTED 상태에서 "Start DFU" 만 누르면 됨

BleManager.kt (demo-final path): currentBluetoothDevice getter 추가
DfuManager.kt: connectDirectly(device, name)
FirmwareUpdateView.kt: LaunchedEffect 개편 — 조건 분기 · delay · auto-connect

연결 없이 진입하면 기존 스캔 flow 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 15:28:23 +09:00
dw.jang 47317b3c5d fix(firmware): 진입 시 메인 앱 BLE 연결 자동 해제 — 스캔 미검출 해결 (cloud-mvp bc46b7f 이식)
문제: 이미 프로브에 BLE 연결된 상태로 펌웨어 업데이트 화면 진입 → 스캔해도
     프로브가 목록에 안 뜸.

원인: BLE peripheral (VBT 프로브) 는 central 과 연결 중일 때 광고 중단
     (표준 BLE 동작). DfuManager 의 새 BleScanner 는 광고를 못 잡음.

해결: FirmwareUpdateScreen 진입 시 LaunchedEffect 안에서 메인 앱의
     BleManager.getInstance(context).disconnect() 호출.
     → 연결 해제 → 프로브 다시 광고 → DfuManager 스캔에서 검출 가능.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 15:09:06 +09:00
dw.jang 984fe2c939 feat(demo-final): cherry-pick — Firmware DFU + Battery dismiss + AppState fix (cloud-mvp 이식)
feature/cloud-mvp 에서 최근 만들어진 3가지 사용자용 기능을 demo-final 에도 적용.
demo-final 옛 폴더 구조 유지 (folder reorg 안 됨) · 각 파일 위치 수동 매핑.

이식 대상:

1. Firmware DFU (cloud-mvp 672e679):
   - firmware/ 폴더 신규 · MEDiDFU 6 파일 이식 (DfuManager 459 line 등)
   - assets/dfu_application_2+35.zip (243 KB · signed MCUboot)
   - libs.versions.toml: mcumgr = "2.5.0" (Kotlin 2.0 호환)
   - build.gradle.kts: mcumgr-ble + lifecycle-runtime-compose 추가
   - AppScreen.FIRMWARE_UPDATE enum 추가
   - MainActivity: 라우팅 + 뒤로가기 (→ HOME)
   - HomeView: "펌웨어 업데이트 (프로브 firmware)" TextButton 추가 (Dev clinical 버튼 밑)

2. BatteryWarningBanner tap-to-dismiss (cloud-mvp aa6cc1f):
   - clickable { dismissed = true } + 우측 X 아이콘
   - WARNING → CRITICAL 악화 시 자동 재표시
   - 재연결 시 dismissed 초기화

3. AppState.isDeviceConnected getter (cloud-mvp 9c82a50):
   - mutableStateOf → getter 로 변경 (BleManager.isConnected.value 실시간)
   - Compose 자동 재구성 · setter 는 no-op 로 호환 유지
   - 홈화면 "연결됨" · Start 후 "연결해제" 불일치 해결

주요 파일 매핑 (feature/cloud-mvp → demo-final):
- core/AppState.kt         → AppState.kt (root)
- core/MainActivity.kt     → MainActivity.kt (root)
- common/ui/components/BatteryWarningBanner.kt → ui/components/BatteryWarningBanner.kt
- firmware/**              → firmware/** (신규 · 동일)
- shell/ui/SettingsTabView.kt (진입점 원본) → HomeView 로 대체 (demo 는 SettingsTab 없음)

검증: BUILD SUCCESSFUL (demoDebug 1m 56s) · APK install PASS

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 13:29:18 +09:00
dw.jang 2e0ca889d9 fix(bv): PiezoMonitoringView Method D BV → estimateBv() wrapper — Python 1:1
D2: 기존 PiezoMonitoringView 는 Method D BV 를 estimateBladderVolume6ch()
직접 호출로 계산 → adaptive_large_bladder_relax + b_si_floor + subsample
refined walls 모두 미적용. Alignment V3 / Clinical Live 는 이미
estimateBv(walls) wrapper 통과 (Python 1:1) 로 정상 동작 중이었음.

- MethodDResult 를 methodDDets 변수에 캐시 (METHOD_D detection 경로 +
  useMethodDBv 분기 둘 다).
- BV 계산 분기에 useMethodDBv 케이스 추가: WallWithSpan(antRefined,
  postRefined, lowStart, lowEnd) 만들어서 estimateBv(walls) 호출.
  antOffset 은 refined ant 에 samples 단위 subtract (기존 Int 경로와
  동일 시맨틱).
- 신규 test MethodDBvWrapperParityTest:
  · wrapper (Monitoring D2) == wrapper (Alignment/Clinical) : bit-exact
  · wrapper vs legacy int-only : data123 33 케이스에서 최대 20.4% 차이
    (96 mL @ 471 mL). Legacy 가 큰 방광에서 systematically 저추정.

3 소비자 경로 (Monitoring · Alignment V3 · Clinical Live) 모두 이제
Python `runners.estimate_bv` 와 완전 동일.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:24:54 +09:00
dw.jang efbaab1d23 fix(methodd): merge_close_spans prominence semantic — Python 1:1
Python `merge_close_spans(gap_peak_min_prom=X)` 는 gap 내 peak 가 양쪽
endpoint valley (max(sg[prev_e], sg[s])) 대비 상대 높이가 X 초과일 때
병합을 스킵한다. Kotlin 은 대신 absolute (peak > low_amp + X) 기준을
써서, valley 가 low_amp 보다 훨씬 낮은 경우 병합 스킵 임계가 Python
보다 높아지는 잠재 divergence 있었음.

- SpanUtils.mergeCloseSpans 에 gapPeakMinProm 파라미터 추가, Python
  우선순위 (prom 이 thr 을 override) 그대로 이식.
- Legacy DetectLumenFirst 는 여전히 gapPeakThr (absolute) 사용 —
  peakCheckMinGap default 를 1 로 유지해 하위호환 (Python default 는 3).
- MethodDSpan 은 신규 gapPeakMinProm 파라미터로 호출 전환.
- SpanUtilsMergeCloseSpansTest 5 케이스 (prom split/merge, gap=0,
  absolute compat, 우선순위) 로 semantic 검증.

Golden test 33 케이스 회귀 없음 (기존 케이스에선 두 semantic 이
동일 결과였음 확인). Method D wall 검출 semantic 이 Python 1:1 도달.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:17:42 +09:00
dw.jang 739d52e472 feat(ble): rec: / ric: chunk reassembly — 저 MTU 기기 지원
Firmware (2026-07-21+) 는 MTU 협상값이 낮아 reb: 210B / rim: 188B 를
못 보낼 때 다중 chunk (rec: / ric:) 로 쪼개 전송함. 앱은 각 chunk 의
offset/total/chunk_samples 헤더를 읽어 채널별로 재조립해 기존 reb: / rim:
완료 경로와 동일하게 downstream 콜백을 발생시킨다.

- PiezoPacketCollector: rec: chunk → channelResults[chNum] 재조립, session/dup
  규칙은 reb: 와 동일. raa: 도착 시 완료 검사.
- ImuPacketCollector: ric: chunk → onComplete(15 samples), offset=0 재도착 시
  이전 partial 파기.
- BleManager.processReceivedData: rec: / ric: 케이스 dispatch + debug log.
- PiezoRecReassemblyTest / ImuRicReassemblyTest: 실측 84+16 / 14+1 split
  케이스 + 재전송/중복/세션 변경 회귀 방지.

정상 MTU 247 기기의 reb: / rim: 경로에는 변경 없음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 18:01:28 +09:00