실리콘 R100/R200/R300 을 번갈아 쓰려면 한 기기를 끊고 다음을 잡아야 하는데
끊을 길이 없었다. 연결돼 있으면 버튼이 "연결 해제"로 바뀐다(병원 임상 모드 ·
정렬 화면 양쪽). disconnect() 가 자동 재연결도 취소하므로 방금 끊은 기기로
되돌아가지 않는다.
기기를 바꾸면 **정렬 결과를 지운다.** R100/R200/R300 은 빔 각도가 서로 달라
(0/-7/-14/-21 vs 5/-4/-12/-21 vs 0/-9/-18/-27) 같은 자리에서도 nch·cap_frac 이
달라진다 — 최적 부착 위치 자체가 다르므로 앞 기기의 cm 을 들고 가면 안 된다.
## dps 동기화 버그
`WdConfig.DPS_DEFAULT` 는 검출 함수들의 기본 인자인데, 이전 커밋에서 그 값을
`PiezoHW.distancePerSample` **getter 안에서만** 갱신하게 만들었다. 프리셋이 바뀐 뒤
아무도 그 프로퍼티를 읽지 않으면 옛 값이 남는다 — 실리콘으로 갈아 끼웠는데 검출은
abs 의 1.936 으로 도는 상황이 정확히 그것이다. activePreset setter 에서 동기화한다.
PresetDpsTest 신규: 프리셋 → dps·delay 표(v*=1.936/6.85, r*=1.897/7.651)와
WdConfig 동기화, 그리고 기기 이름 → 프리셋 매핑 6종을 검증한다.
testOptions.unitTests.isReturnDefaultValues=true — android.util.Log 스텁 때문에
순수 계산 시험이 죽던 것을 막는다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
실측 세션(HUMAN-kai VBT26050202, v1) 2위치로 stage 대조한 결과 다섯 곳이
레퍼런스와 어긋나 있었다. 전부 고쳐 **nch · ch3 · ch3_rate · cap_frac 이 전 항목
일치**한다.
| # | 어긋난 곳 | 값 | 증상 |
|---|---|---|---|
| 1 | `d_max` | 10 → **15** | 후벽 탐색창 부족 |
| 2 | `inward_walk_win` | 3 → **4** | span(low_start/low_end) 이탈 |
| 3 | prominence 기준점 | 인접 골 → **span 바닥** | 전벽 오선택 (py 4 vs kt 13) |
| 4 | cap 축 붕괴 폴백 | **누락** → 구현 | BV −14.9 mL |
| 5 | dps · delay | 전역 상수 → **프리셋 유도** | 좌표 전체 이탈 |
1·2·5 는 레퍼런스가 갱신된 것을 포팅이 따라가지 못한 경우다. 3 은 SPEC 이
"특히 틀리기 쉬운 곳"으로 짚어 둔 항목(`ANT_FLOOR_REF`)이고, 4 는
`runners.estimate_bv` 의 `CAP_AXIS_RATIO_MAX` 블록이 통째로 빠져 있었다.
곁들여 바로잡은 것:
- `ELLIPSE_CAP_R_MAX` 0.92 적용 (종전 사실상 1.0)
- `TOP_CAP_EDGE_K` 0.6 (최상단 채널이 최광폭일 때 top cap 제한) — 누락돼 있었다
- top cap clamp 게이트를 `n < nTotalCh` → **center 검출 채널 ≥ 2** 로 (레퍼런스 변경 반영)
- `estimateBv` 의 조기 return 들이 cap 축 폴백을 건너뛰던 것 수정. 특히
`validChannels.size < 4` 때문에 채널 하나만 빠져도 폴백이 사라졌다.
## ⚠ dps 기본값이 바뀐다
종전 전역 기본 1.968 은 "300ml 팬텀이 1.936 에서 130~140ml 로 나온다"는 관찰로
되돌렸던 값인데, 프리셋이 정한 값을 전역 상수가 덮는 구조라 레퍼런스 대조가
불가능했다. 이제 v0/v1/v2 = 1.936 · 6.85, r1/r2/r3 = 1.897 · 7.651 로 프리셋에서
유도한다. 팬텀 시연에서 스케일이 달라 보이면 dev 패널에서 dps 를 직접 지정하면
된다(`PiezoHW.distancePerSample`, 해제는 `clearDpsOverride()`).
## 검증
- 전처리(light/heavy) max|Δ| = 0
- CCC-off 검출 12/12 채널 일치 (ant·post·refined·span)
- CCC-on 검출 12/12 채널 일치
- 정렬 지표(nch·ch3·ch3_rate·cap_frac) 2위치 전 항목 일치 — AnchorMeasureParityTest
의 @Ignore 를 떼어 상시 게이트로 전환
- BV 제품 경로 고정 walls 155.4257 mL 일치 — BvProductPathParityTest 신규
(cap 축 폴백이 빠지면 140.5 로 떨어져 여기서 잡힌다)
- 선택 규칙 200/200 은 그대로 유지
정렬 화면의 "검증 전" 경고를 걷어내고 매니페스트
`selection_verified_against_reference` 를 true 로 바꿨다. 원시 데이터 저장은
유지한다 — 재판정 여지를 남기는 것은 검증과 별개다.
남은 것: 이 다섯 가지는 신 저장소(vesiscan_pre_product)의 같은 코드에도 그대로
있다. 임상 끝나고 이관해야 한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
piezo-phantom-test `vesiscan_test` 의 단일 pass 정렬을 이식한다. 규칙은
`alignment_selection.select_supine_anchor`(tie='cap'), 절차는
`alignment_runners.AnchorGuide`, 계약은 `porting/SPEC.md` §8.
- managers/AnchorGuide.kt 알고리즘 코어(측정·종료판정·선택 위임·안내)
- ui/.../AnchorAlignView.kt 위치별 측정 화면 + 원시 데이터 저장
- HospitalRunStore align/ 폴더에 위치별 raw cycle + 결과 요약
- 병원 모드에 정렬 카드 · 매니페스트에 anchor_cm
기존 AlignmentAdvisorV3 는 같은 저장소의 **구 알고리즘**(2-pass CenterAligner ·
bvcv tie · 오프셋 없음)이고 호출처가 0이라 그대로 두었다. Python 쪽에서도 이미
주석 처리됐다. 제품 경로는 새 파일이다.
## 검증
- 선택 규칙: Python 무작위 200 케이스 전수 대조 **200/200 일치**
(CH3 2단 게이트 · fallback · NaN=+inf · 동률 min cm 경계 포함)
- trace 분할: 22 cycle · win 10 → 11 trace, Python 과 일치
- 실기기: 20 cycle 수집 → 판정 → 안내 → 저장까지 완주 확인
## 알려진 불일치 — 화면에도 적었다
앱의 **벽 검출**(MethodDRunner)이 현재 Python 레퍼런스와 어긋난다. 실측
(HUMAN-kai VBT26050202, v1):
cm=0 CH2 ant 13.913 vs 18.559 · span 24~61 vs 33~60
cm=1 CH1 ant 4.486 vs 13.188 · CH3 는 Python 검출 / 앱 미검출
cm=1 판정 nch=4 ch3=O vs nch=3 ch3=X
cm=0 cap_frac 0.63899 vs 0.67479 · BV 410.6 vs 470.5 mL
cm=1 은 레퍼런스에서 유력 후보인데 앱에서는 후보 자격조차 없다. 정렬 선택이 이
값들 위에 서 있으므로 **앱의 추천 위치는 아직 레퍼런스의 답이 아니다.**
그래서 (1) 화면에 검증 전임을 명시하고, (2) 위치별 raw cycle 을 반드시 남긴다
(align_{n}cm.csv · Python 대조 스크립트가 바로 읽는 형식). 앱 판정이 틀려도
나중에 Python 으로 다시 고를 수 있어야 그 환자를 다시 부르지 않는다.
AnchorMeasureParityTest 는 이 불일치를 표로 기록한 채 @Ignore 로 격리했다 —
기대값을 코틀린 값으로 낮추면 대조 시험의 존재 이유가 사라진다.
BV **코어** 자체는 맞다. 벽을 고정해 넣으면 Python estimate_bv
(ellipse_cap_height=true · cap_fit='specific')와 마지막 자리까지 일치한다.
## 곁가지
- gradle.properties 에서 -Dfile.encoding=UTF-8 제거. 한글 사용자 폴더에서
테스트 워커가 클래스패스를 못 찾아(GradleWorkerMain) **단위시험이 통째로
실행되지 않고 있었다**. 신 저장소에서 같은 원인을 이미 확인했다.
- 환자명을 AppState 로 올렸다. 정렬 화면을 다녀오면 remember 가 초기화돼,
이름을 다시 치는 순간 "환자가 바뀌었다"로 보여 방금 맞춘 정렬이 지워졌다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
간호사가 자세·방광 단계를 하나씩 올려 가며 재는데, 중간에 자리를 비우거나
순서가 꼬이면 "0% 했던가?"를 기억으로 판단하게 된다는 현장 의견. 그러면 한
단계를 통째로 빠뜨리거나(방광을 다시 채워야 한다) 같은 조건을 두 번 재게 된다.
화면이 대신 기억하게 한다.
- 방광 단계 칩: 지금 고른 자세 안에서의 진행. 완료 ✓(초록) / 일부 ·(주황 테두리)
- 자세 칩: 그 자세의 6단계가 전부 끝나야 완료
- 실행이 끝날 때마다 다시 읽어 방금 잰 단계가 즉시 채워진다
판정 근거는 **파일 존재가 아니라 실행 매니페스트**다. CSV 는 첫 회차만 저장돼도
만들어지므로, 파일 유무로 보면 20회 중 2회에서 끊긴 것도 "완료"로 보인다. 그게
제일 위험하다 — 다 됐다고 믿고 방광을 비우는데 데이터는 못 쓰고 그 단계는 그날
다시 못 잰다. 매니페스트의 조합별 planned/saved 로 "6조합이 전부 계획한 횟수를
채웠는가"를 본다. 중단된 실행도 매니페스트가 남으므로 PARTIAL 로 잡힌다.
완료/일부를 색뿐 아니라 기호(✓ ·)로도 구분한다 — 병실 조명·화면 반사에서
색만으로는 잘 안 보인다.
검증: 실기기(VBT26080001 연결)에서 오늘 09:40 실측 데이터로 확인.
(Supine, 0%) → 0% 칩 ✓ · Supine 칩 · (6단계 중 1단계라 일부). 선택을 20% 로
옮기면 0% 가 초록 채움으로 남는 것까지 확인. 측정 경로는 건드리지 않았다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"기기를 먼저 연결해 주세요" 라고 안내만 하고 정작 가는 길이 없었다. 연결이 이 모드의
첫 단계인데, 조작자는 홈까지 되돌아가 일반 흐름을 타고 다시 들어와야 했다.
## 복귀처를 불리언이 아니라 값으로
기기 연결 화면은 여러 곳에서 불려 온다. 원래는 `ClinicalSessionStore.inClinicalFlow`
불리언 하나로 "임상 흐름이면 CLINICAL_HOME, 아니면 HOME" 만 갈랐다. 여기에 병원 모드용
불리언을 하나 더 만들면 그 순간부터 두 깃발이 어긋나기 시작한다.
그래서 `AppState.connectReturnScreen` 에 **어디로 돌아갈지를 값으로** 담는다. null 이면
기존 규칙 그대로라 **기존 호출부는 하나도 건드리지 않았다.** 한 번 쓰면 비운다
(consumeConnectReturn) — 안 비우면 다음에 다른 경로로 들어온 연결 화면까지 엉뚱한
곳으로 보낸다.
## 나가는 길이 셋이라 셋 다 손봤다
· 연결 성공 AppState.deviceConnected()
· 시스템 뒤로가기 MainActivity BackHandler
· 화면 안 화살표 AppState.backToSensorSelect()
세 번째에서 기존 문제도 드러났다 — 화면 안 화살표는 `inClinicalFlow` 를 보지 않고 늘
HOME 으로 간다. 시스템 뒤로가기와 목적지가 다르다. 기존부터 그랬고 지금 고치면 임상
흐름의 동작이 바뀌므로 그대로 두고 주석으로 남겼다. 복귀처를 지정한 화면만 두 경로가
같아진다.
## 검증 (실기기 23021RAA2Y)
홈 3연타 → 개발자 모드 → [병원 임상 측정] → [기기 연결] → 검색 화면 →
시스템 뒤로가기 / 화면 안 화살표 **둘 다** 병원 임상 화면으로 복귀. 크래시 0건.
연결 성공 경로는 프로브가 없어 아직 확인 못 했다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## 앞선 커밋이 세 군데 틀렸다
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