Commit Graph

455 Commits

Author SHA1 Message Date
dw.jang 53fd07c342 feat(clinical): 병원 임상 측정 모드 — 주파수×cycle 6조합 자동 순회
## 조작자가 하는 일은 하나다
간호사가 방광을 정해진 정도까지 채워 두면, 조작자는 **그 채움 정도만** 고르고
[측정 시작]을 누른다. 이후 주파수 2종 × cycle 3종 = 6조합을 앱이 자동으로 순회하며
각 조합마다 n회(기본 20) 측정하고 조합별 파일로 저장한다.

조합을 손으로 바꾸게 두면 반드시 빠뜨리거나 잘못 기록한다. 이미 채워 둔 방광은 다시
만들 수 없으니, 그 자리에서 놓친 조합은 그날 데이터에서 영영 빈다. 사람이 개입하는
지점을 하나로 줄인 것이 이 모드의 핵심이다.

  진입: 홈에서 캐릭터 3연타 → 개발자 모드 → [병원 임상 측정]

## 프로토콜
  자세      Supine / Sitting / Standing (기존 ClinicalPosture 재사용)
  주파수    1.8 · 2.3 MHz          → mpa? 첫 인자
  cycle     3 · 5 · 7              → mpa? 둘째 인자
  방광 채움 0/20/40/60/80/100 %    → 사람이 아는 값 = 정답 라벨
  반복      화면 입력 (기본 20)

## 저장
  Downloads/VesiScan_Hospital/{날짜_환자}/
    2026-09-03_홍길동_Supine_040pct_1.8MHz-fopt1_c3.csv
    run_HHmmss.json   ← 계획 대비 실제 저장 수

파일명에 조건을 전부 적는다. 폴더로만 구분하면 파일 하나를 옮기는 순간 조건을 잃는데,
임상 데이터는 나중에 다른 사람이 모아서 분석한다. CSV 열 구성은 기존 AdcCsvLogger 와
맞춰(scan_id·timestamp·channel·s0~s99) 분석 스크립트를 새로 만들지 않아도 되게 했다.

기존 임상 R&D(`VesiScan_Sessions/`)와 폴더를 나눴다 — 목적도 구조도 달라서 섞이면
나중에 파일을 하나씩 열어 봐야 한다.

## ⚠ 주파수 코드값이 확인 전이다
`mpa?` 첫 인자는 정수인데 그 정수와 MHz 의 대응이 코드에도
docs/BLE_PROTOCOL_REFERENCE.md 에도 없다. 기존 코드는 늘 `freqOption = 2` 만 썼다
(PlacementGuideView · MeasurementService). 그래서 1.8→1, 2.3→2 는 **가정**이다.

확인 전에 모은 데이터가 버려지지 않도록, 실제로 보낸 정수를 파일명(`fopt1`)과 CSV 열
(`freq_option`), run json 에 함께 남긴다. 대응이 반대로 밝혀져도 라벨만 바꾸면 된다.
run json 에 `freq_option_mapping_confirmed: false` 를 박아 두어 나중에 이 데이터가
어떤 상태에서 모였는지 알 수 있게 했다. 화면에도 같은 경고를 띄운다.

## 데이터 정합성
결과를 평범한 var 로 주고받으면 BLE 스레드↔코루틴 간 가시성이 보장되지 않아 Channel
을 쓴다. 그리고 **회차마다 보내기 전에 채널을 비운다** — 직전 회차가 시간초과된 뒤
뒤늦게 도착한 결과가 남아 있으면 이번 회차 데이터로 잘못 기록된다.

한 회차가 3초 안에 안 오면 실패로 세고 다음으로 넘어간다. 한 번 막혔다고 전체가
멈추면 채워 둔 방광을 버리게 된다. 실패 수는 화면과 run json 에 남는다.

## 검증
빌드 통과. **실기기 확인은 아직 못 했다** — 폰이 절전 상태로 들어가 화면을 못 띄웠고,
프로브 연결 상태의 측정 루프는 전혀 돌려보지 못했다. 내일 임상 전에 반드시 한 번
돌려봐야 한다(6조합 × 20회 = 120측정 · 약 2~3분 예상).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 16:53:47 +09:00
dw.jang 5f40896bee build: 앱 이름을 vesiscan-demo 로 · applicationId 충돌 제거
이 브랜치는 동결 시연판이다. 이제 앱이 셋으로 늘어 한 폰에 같이 깔리므로, 이름과
applicationId 를 1:1 로 못 박아 무엇이 무엇인지 헷갈리지 않게 한다.

  vesiscan-demo       com.medithings.vesiscan.demo        ← 이 브랜치
  vesiscan-user       com.medithings.vesiscan             ← vesiscan_pre_product
  vesiscan-caregiver  com.medithings.vesiscan.caregiver   ← vesiscan_pre_product

## 이름
"VesiScan-Basic Demo"(demo) · "VesiScan Dev"(dev) 를 각각 vesiscan-demo ·
vesiscan-demo-dev 로. 예전 이름은 **무슨 앱인지가 아니라 어떤 빌드인지**만 알려 줘서,
폰에 여러 개가 깔리면 구별할 수 없었다.

## applicationId — 이게 진짜 문제였다
base 가 `com.medithings.vesiscan` 이고 demo 플레이버가 `.demo` 를 붙이는 구조라,
**dev 플레이버는 접미가 없어 `com.medithings.vesiscan`** 이었다. 그 id 는 새 저장소의
환자 앱과 정확히 같다 — 이 브랜치에서 devDebug 를 한 번 빌드해 깔면 환자 앱이
말없이 덮어써진다. 이름을 아무리 잘 붙여도 막을 수 없는 종류의 사고다.

base 를 `com.medithings.vesiscan.demo` 로 옮기고(=demo 빌드의 id 는 그대로),
demo 는 접미 없음, dev 는 `.dev` 를 붙인다. 이제 어떤 플레이버를 빌드해도 다른 앱을
건드리지 않는다.

검증: aapt2 dump badging — package `com.medithings.vesiscan.demo` ·
label `vesiscan-demo` · versionName `2.0.0-demo`. assembleDemoDebug PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:31:01 +09:00
dw.jang f025bbc0a3 feat(longrun): 장시간 측정 중단 감지 + 설정에 준비 점검 항목
같은 빌드가 태블릿 34시간 / Xiaomi 40분 이었던 문제 대응.
앱 구조(Foreground Service + connectedDevice + WakeLock)는 이미 맞았고
제조사 배터리 관리자가 프로세스를 죽인 것이라, 구조 변경이 아니라
감지와 안내로 접근한다.

- LongRunGuard: 측정 시작/생존/정상종료를 기록해 비정상 종료를 감지.
  다음 실행 때 "언제부터 언제까지 몇 분" 을 알려준다. 제조사 무관하게 동작.
- BLE 로그 헤더에 기기/제한 상태 진단줄 (실패 후 원인 판별용).
- 설정 패널에 "장시간 측정 준비" — 배터리 최적화·미사용앱 제한 상태 확인 및
  해제 진입. 자동시작은 상태를 읽을 수 없어 안내만 제공.

측정 시작을 모달로 막지 않는다. 초안에서는 Auto Scan 에 사전점검을 걸었는데
OEM_AUTOSTART 를 제조사 이름만으로 판단해서(상태 조회 API 가 없음) 설정을
완벽히 해둔 기기에도 영원히 뜨는 문제가 있었다. 고칠 수도 없는 항목으로
측정을 가로막으면 경고가 무시된다. 추측으로 경고하지 않고, 실제로 중단됐을
때만 알린다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 11:13:46 +09:00
dw.jang a86fefe321 fix(ble-log): 앱 시작~종료를 한 파일에 통으로 자동 저장
Downloads 에 VesiScan_BLE_*.log 가 여러 개 생기고 그마저 내용이 비던 문제.
원인이 셋이었다.

1. 재연결마다 파일이 갈라짐
   connected() 가 liveLogFile=null 로 핸들을 버려서 BLE 가 끊겼다 붙을 때마다
   새 파일이 생겼다. 앱 시작~첫 연결 구간도 별도 파일로 빠졌다.
   → 프로세스 1회당 파일 1개. 재연결 시에는 구분선만 남긴다.

2. 파일 쓰기에 동기화가 없음
   rx() 는 BLE 콜백 스레드, 나머지는 UI 스레드에서 호출되는데 매번 파일을
   새로 열어 쓰며 잠금이 없었다. 동시 쓰기로 줄이 섞이거나 유실됐다.
   → synchronized + 핸들 유지, 줄마다 flush (강제 종료에도 그 시점까지 보존).

3. 저장 실패가 조용히 묻힘
   Downloads 쓰기가 막히면 예외를 삼켜 아무 데도 안 남았다.
   → 내부 저장소로 폴백하고 실제 경로를 파일 헤더에 기록.

추가로 두 가지:

- Export 버튼이 메모리 링버퍼(2000줄)를 덤프해 "마지막 몇 분"만 나오던 것을
  실시간 세션 파일을 그대로 내보내도록 변경.
- disconnected() 가 비정상 종료 경로에서만 호출돼, 사용자가 직접 끊으면
  아무 기록 없이 로그가 끊겼다. 이유(user/unexpected/gatt_error)를 구분해
  항상 남기도록 수정.

임상 모드 step 별 ble.log 는 부가 사본으로 그대로 유지한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:54:38 +09:00
dw.jang 1d08c5e0cb chore(gitignore): ppt/ 추적 제외
발표자료·외부 참고문서(가이드라인 PDF·펌웨어 zip·엑셀 임시파일)가
untracked 로 계속 뜨던 것 정리. feature/cloud-mvp 와 같은 정책.

이미 추적 중인 ppt/ 하위 5개는 그대로 둔다 (.gitignore 는 tracked 파일에
적용되지 않으며, 작업물이라 임의로 추적 해제하지 않음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:09:45 +09:00
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 1f2fc56008 chore(gitignore): demo-final 사이버보안 문서 · shared/ 추적 제외
배경: 사용자 정책 "demo-final = 시연 전용 · 사이버보안 무관" 확정.
docs/cybersecurity/ 21개 HTML 문서와 shared/ 폴더가 untracked 상태로
git status 를 계속 오염시킴.

추가:
  · docs/cybersecurity/  — 사이버보안 문서는 feature/cloud-mvp 만 tracked.
                          demo-final 로컬에는 참고용으로 남되 git 은 무시.
  · shared/              — :shared 모듈은 cloud-mvp 전용.
                          demo-final 에는 shared/build stale artifact 만 남음.

효과:
  · git status 깔끔
  · cloud-mvp 로 checkout 시엔 두 폴더 실제 파일 정상 관리 (해당 브랜치 tracked)
2026-08-14 10:07:24 +09:00
dw.jang a89936c862 docs(algo): ALGO_BRANCH_COMPARISON §3-4 METHOD_D vs METHOD_D_PHANTOM 심층 비교
기존 §3 BV Estimation 은 브랜치 비교 관점만 · 두 method 자체를 나란히 놓고
상세 대조하는 섹션 없어서 §3-4 로 추가 (사용자 요청).

포함 (7 sub-section):
  3-4-1. 부피 공식 (Frustum+Cap vs 4/3πr³)
  3-4-2. 파이프라인 stage 비교 표 (11 stage · METHOD_D 만 있는 것 명시)
  3-4-3. 결과 특성 표 (정확도 · 계산 비용 · 재현성 · phantom vs 인체)
  3-4-4. 예시 계산 (phantom 300mL · 지름 83mm · 두 method 각각 산수)
  3-4-5. 언제 어느 method 써야 하나 (권장 매트릭스)
  3-4-6. 코드 진입점 요약 (dispatcher)
  3-4-7. 향후 개선 여지 (하이브리드 · ellipsoid 확장 등)

핵심 근거:
  · phantom 300mL 실측 → METHOD_D_PHANTOM 296mL (-1.3%) vs METHOD_D 250mL (-17%)
  · 원인: 큰 대칭 방광은 구 가정이 정확 · Frustum+cap 은 adaptive 로도 저추정
2026-08-13 14:42:35 +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 10d7932ab2 docs(review): CODE_REVIEW_2026-08-12 최종본으로 확장
기존 b1f7b9f 초안 → 완전 상세 리라이트.

추가된 내용:
  · 각 파일별 정확한 삭제 라인 (enum · 함수 · when-branch · 콜백 등)
  · Category B build.gradle deps 삭제 코드 블록 (demo/cloud 각각)
  · Category B AndroidManifest 삭제 XML 블록
  · Stale import fix 2단계 상세 (a24a8d3 UserStorage/AppState · fd98a04
    ClinicalHomeView/MeasurementHistoryView) + 최종 grep 검증 결과
  · Demo flavor 앱 이름 override (517a93d) 배경 · 조치 · 최종 표
  · 브랜치 커밋 체인 시각화
  · Elite Programmer 관점 5개 lessons learned:
    1) Wildcard import 함정
    2) Explore agent 참조 검증 한계
    3) Flavor-specific res 오버라이드 locale 이슈
    4) SharedPrefs 마이그레이션 안전 패턴
    5) 카테고리 분리 커밋 정책
  · 감소 규모 최종 집계 (LOC + Asset + APK 크기 · build 시간)
  · Category D (유지 권장) 잠재 ~1,800 LOC 다음 스텝
  · 참고: 지난 세션 커밋 링크
2026-08-12 17:32:13 +09:00
dw.jang 517a93d717 feat(ui): demo flavor 앱 표시 이름 "VesiScan-Basic Demo" 오버라이드
이전:
  · app/src/demo/res/values/strings.xml 은 "VesiScan Demo" 로 되어 있었으나
    values-ko/ override 없음 → 폰이 한국어 locale 이면 main 기본값
    "VesiScan-Basic" 그대로 표시 → dev flavor 와 앱 이름 구분 안 됨.

수정:
  · values/strings.xml    : "VesiScan Demo" → "VesiScan-Basic Demo" (사용자 지시)
  · values-ko/strings.xml : 신규 · "VesiScan-Basic Demo" 동일

결과:
  · demo flavor apk (com.medithings.vesiscan.demo) → 홈 화면 "VesiScan-Basic Demo"
  · dev flavor apk  (com.medithings.vesiscan)       → 기본 "VesiScan-Basic" 유지
  · 두 앱이 폰에 공존해도 이름으로 명확히 구분됨.

Cloud-mvp 는 이 커밋 미이식 (사용자 지시 demo-final 만).
빌드: BUILD SUCCESSFUL 1m 2s · installDemoDebug 완료.
2026-08-12 16:54:51 +09:00
dw.jang b1f7b9fa0e docs: 2026-08-12 코드 정리 & 리뷰 산출 문서
3개 카테고리 (A NIRS · B YOLO · C 참조 0) 정리 결과 통합 문서.

포함:
  · 카테고리별 실제 제거 항목 · LOC · 커밋 해시 (demo/cloud 각각)
  · 총 ~2,378 LOC + 10.6 MB APK 감소
  · Category D (유지 권장) 후속 검토 리스트
  · Explore 조사 오탐 회고 (DfuBleScanner/DfuNus/DfuNusConnection 실사용 확인)
  · 남은 개선 여지 (시그니처 축소 · enum orphan · SharedPrefs 마이그레이션)
  · 브랜치별 커밋 체인 · 검증 상태

브랜치 최종:
  · demo-final:     0fe0687 → 1ce1994 → 26dc53b → b028060
  · feature/cloud-mvp: 844e0c0 → 7ff166f → dce9cca → 13955bc
2026-08-12 16:16:17 +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 e414a41a6d docs(algo): ALGO-04 demo-final vs feature/cloud-mvp 브랜치 비교 문서
3축 (Alignment / Detection / BV) 로직 상세 비교.

핵심 정리:
  · demo-final = phantom (구) 시연 전용 · V=4/3πr³ · dps=1.968 · METHOD_D_PHANTOM
  · cloud-mvp   = 인체 (타원) 실사용 · Halir+adaptive · dps=1.936 · METHOD_D
  · 두 브랜치 값이 다른 것이 정상 설계 (기하가 다름).

각 축별 default 표 · 로직 diff 요약 · 관련 파일 경로 (line 번호 포함).
브랜치 전용 파일 (demo: AlignmentAdvisorV3 · estimateBvPhantomSphere ·
cloud: StreamingBladderEstimator · VDIP post recovery · audit-logger).
향후 유지 원칙 (무분별 이식 금지 · phantom 재현성 우선).

Docmost 업로드: https://docmost.medithings.net/s/vesiscan/p/IZyb46bKVH
2026-08-11 16:42:12 +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 395e908a01 chore(version): 2.0.0 (versionCode 200)
시작 화면 (HomeView) 에 표시되는 버전을 2.0.0 으로 갱신.
  · versionName: 1.1.1-design → 2.0.0
  · versionCode: 27 → 200 (2.0.0 semantic 매칭 · downgrade 방지)

demo flavor 는 -demo 접미사 그대로 → 실제 화면 표기: "2.0.0-demo".
빌드 통과 (:app:compileDemoDebugKotlin).
2026-08-10 17:36:26 +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