## 앞선 커밋이 세 군데 틀렸다
2026-09-02 펌웨어팀 스펙을 받아 바로잡는다.
1. **인자가 2개가 아니라 5개다.**
mcs? [tag 4B][freq 2B][cycles 2B][avg 2B][delay_us 2B][samples 2B][crc 2B] = 16B
avg·delay_us·samples 를 안 보내면 길이 부족으로 거부된다(freq=0xFFFF 응답).
2. **주파수 값이 1/2 가 아니라 0/5 다.**
0=1.8 · 1=1.9 · 2=2.0 · 3=2.1 · 4=2.2 · 5=2.3 MHz.
앞선 커밋의 가정(1.8→1, 2.3→2)은 둘 다 틀렸다 — 실제로는 2.0 과 2.3 을 재게 된다.
가정을 파일명에 남겨 둔 안전장치가 없었다면 못 알아챌 뻔했다.
3. **응답을 확인하지 않고 있었다.** 이게 가장 위험하다 — 아래 참고.
## 설정 실패는 조용하다 — 그래서 반드시 확인한다
실패해도 `rcs:` 는 온다. 구분은 freq 값이다:
· 0xFFFF — 파라미터 범위 초과 또는 데이터 길이 부족
· 0xFFFD — 검증은 통과했으나 NVS 저장 실패
거부돼도 프로브는 **옛 설정으로 측정을 계속한다.** 확인하지 않으면 파일에는 요청한
값이 적힌 채 다른 조건의 데이터가 쌓인다 — 잘못된 데이터가 정상처럼 보이는, 임상에서
가장 나쁜 결과다.
그래서 조합마다 echo 를 받아 **요청한 다섯 값과 전부 일치할 때만** 측정한다.
불일치·무응답이면 그 조합을 통째로 건너뛰고 run json 에 `skipped_reason` 을 남기며
화면에 빨갛게 띄운다. 비는 편이 틀린 것보다 낫다.
## 고정 파라미터
프로토콜이 바꾸는 것은 주파수·cycle 뿐이다. 나머지 셋은 모든 조합에서 같아야 비교가
성립하므로 `HospitalFixedParams` 한 곳에 둔다 — avg 10 · delay_us 10 · samples 100
(펌웨어팀 예시값, samples 는 앱 채널 버퍼 100 과도 일치).
1.8MHz·c3 6D 63 73 3F 00 00 00 03 00 0A 00 0A 00 64 3C 4A
2.3MHz·c7 6D 63 73 3F 00 05 00 07 00 0A 00 0A 00 64 36 FC
## ⚠ 설정이 프로브에 영구 저장된다 (NVS)
전원을 껐다 켜도 유지된다. 즉 이 모드로 측정하고 나면 **일반 측정 화면도 마지막
조합(2.3MHz·cycle 7)으로 동작한다.** run json 에 남기고 화면에도 명시했다.
임상 후 원래 값으로 되돌릴지는 별도 결정이 필요하다 — 공장 기본값을 모른다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## 확인된 사실 (2026-09-02 펌웨어팀)
송신 주파수·cycle 설정 명령은 **`mcs?`** 다. `mpa?` 는 piezo power ON 이다.
이 앱은 여태 `mpa?` 에 [freq, cycles] 를 실어 보내며 그것이 주파수 설정이라고
가정하고 있었다(PlacementGuideView 의 `sendPiezoPowerOn()`, MeasurementService 의
`sendBurst(freqOption = 2, ...)`). `mcs` 는 코드에도 docs/BLE_PROTOCOL_REFERENCE.md
에도 **한 글자도 없다**.
즉 **지금까지 앱은 주파수·cycle 을 한 번도 바꾼 적이 없다.** 프로브 기본값으로만
측정해 온 셈이고, 그 사실을 아무도 몰랐다. 병원 임상 모드를 만들며 명령을 되짚다
드러났다.
## 고침
`BleManager.sendPiezoConfig(freqOption, cycles)` 를 추가하고 병원 임상 모드가 조합
진입마다 이것을 부른다. `sendPiezoPowerOn`(mpa)은 기존 호출부가 있어 그대로 둔다 —
power ON 은 그 나름의 역할이 있을 수 있고, 확인 전에 건드리면 정렬 화면이 깨진다.
응답 태그는 `sendRaw` 가 m→r 로 바꿔 `rcs` 를 기다린다. 파서의 when 에 rcs 분기가
없어도 태그 처리가 when 밖에 있어 큐는 정상 해제된다(BleManager L1783).
## ⚠ 아직 확인 안 된 것 — 인자 구성
명령 **이름만** 확인됐다. 인자의 개수·순서·인코딩은 듣지 못해, 이 프로토콜의 다른
수치 명령과 같은 규약을 따른다고 **가정**했다:
mcs? = 6D 63 73 3F | freq(BE 2B) | cycles(BE 2B) | CRC16-CCITT(LE 2B) 총 10 B
(1,3) 6D 63 73 3F 00 01 00 03 42 64
(2,5) 6D 63 73 3F 00 02 00 05 D4 5D
값↔MHz 대응도 여전히 모른다. 파일에 실제 전송 정수를 남기는 안전장치는 그대로다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## 조작자가 하는 일은 하나다
간호사가 방광을 정해진 정도까지 채워 두면, 조작자는 **그 채움 정도만** 고르고
[측정 시작]을 누른다. 이후 주파수 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>
이 브랜치는 동결 시연판이다. 이제 앱이 셋으로 늘어 한 폰에 같이 깔리므로, 이름과
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>
프로브 배터리 소모율 실측을 위해 홈 화면(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>
배경 (사용자 관찰):
· 폰 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.
이전:
· 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 (첫 시도 통과).
증상 (사용자 관찰 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 명령 · 응답 태그 표).