당초 조사 대상 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 명령 · 응답 태그 표).
이번에 새로 만든 실리콘 렌즈 타입 목업 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>
target 설정된 상태 (auto-connect 성공) 에서는 Scan 버튼 · Devices 리스트 숨김.
target 없거나 IDLE/SCANNING phase 에서만 노출.
Fallback: 연결 없이 진입하거나 auto-connect 실패 시 기존 스캔 UI 유지.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
이전 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>
문제: 이미 프로브에 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>
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>
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>
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-21) : 최종 BV 보정계수보다 최초 divergence 단계부터 원인
을 찾아 일치. 단계별 중간값 저장 + 자동 diff + 단계별 tolerance.
파일 구성:
- scratchpad/golden_dump_python.py — Python 기준 dump (33 case)
- GoldenDumpTest.kt — Kotlin dump
- scratchpad/golden_diff.py — 단계별 tolerance 자동 비교
Dump 단계 (stage 명명):
E_walls_cross : MethodD detect + cross-channel 후 walls (refined + span)
M_bv : 최종 BV (volume_ml/mm3, valid_channels, D_mm, cap_*)
# 향후 확장 가능 : B (preproc), C (TGC), G (yw/zw), H (Halir 중간), etc.
허용 오차 (단계·필드별):
E_walls_cross : 1e-12 (double eps 근처, ULP 노이즈 흡수)
M_bv.volume_ml : 1e-3 (sub-microliter)
M_bv.cap_b_si/c_ap/y0/z0 : 1e-6 (Halir 결과)
M_bv.cap_mean_residual : 1e-9 (극도 정확)
M_bv.valid_channels : bit-exact (정수/카테고리)
... (총 21 fields)
실행:
1. python scratchpad/golden_dump_python.py
2. ./gradlew testDevDebugUnitTest --tests "*GoldenDumpTest*"
3. python scratchpad/golden_diff.py
Fixture (data123, 총 33 case):
cm0 (VBT26050202 supine 0CM) × 11 trace
cm1 (VBT26050202 supine 1CM) × 11 trace
cm3 (VBT26040302 supine 3CM) × 11 trace
현재 상태:
- 21 필드 × 33 케이스 = 693 검사 지점 모두 tolerance 내 PASS
- 실질 divergence 0. BV max_diff = 3.7e-9 mL (nano-mL 수준).
향후 stage 추가 (B/C/G/H) 는 필요 시 EllipseFitSpecific.debug* 처럼 각 함수의
intermediate 를 노출하는 방식으로 확장. golden_diff.py 의 TOLERANCES 사전에
key 추가만으로 자동 검사 대상 편입.