이 브랜치는 동결 시연판이다. 이제 앱이 셋으로 늘어 한 폰에 같이 깔리므로, 이름과
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>
같은 빌드가 태블릿 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>
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>
발표자료·외부 참고문서(가이드라인 PDF·펌웨어 zip·엑셀 임시파일)가
untracked 로 계속 뜨던 것 정리. feature/cloud-mvp 와 같은 정책.
이미 추적 중인 ppt/ 하위 5개는 그대로 둔다 (.gitignore 는 tracked 파일에
적용되지 않으며, 작업물이라 임의로 추적 해제하지 않음).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
프로브 배터리 소모율 실측을 위해 홈 화면(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>
배경: 사용자 정책 "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)
기존 §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 로도 저추정
배경 (사용자 관찰):
· 폰 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.
기존 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 다음 스텝
· 참고: 지난 세션 커밋 링크
이전:
· 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 완료.
당초 조사 대상 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.
배경: 소변컵 사진 촬영 → 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 (첫 시도 통과).
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):
· 설정 → 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 완료.
브랜치 정책 명확화:
· 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.
사용자 관찰: 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.
변경 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 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): 맨 첫 화면 (Onboarding) 에도 버전 노출.
기존엔 HomeView 에만 있음 · 첫 화면 진입 시 확인 어려움.
변경:
· OnboardingView subtitle 아래에 versionName (12sp · secondary text)
· packageManager.getPackageInfo().versionName 사용 → demo flavor 는
자동으로 "-demo" 접미사 반영 → "2.0.0-demo" 표시.
· runCatching · 실패 시 빈 문자열 (필수기능 영향 없음).
증상 (사용자 보고 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 · 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-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): 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) 회귀 확인이
필요해 롤백.
시작 화면 (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-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):
센서 부착 안 한 상태에서 "정렬 시작" 눌러도 화면상 아무 반응 없음.
원인:
이전 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):
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
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):
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):
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 이 뒤쳐진 상태였음.
배경 (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
배경 (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):
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 · 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 팀 전달 예정.
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):
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 명령 · 응답 태그 표).