Commit Graph

9 Commits

Author SHA1 Message Date
dw.jang 2126aa9361 feat(clinical): 부착 위치 정렬(AnchorGuide) — 임상 프로토콜 전 단계
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>
2026-09-03 10:57:17 +09:00
dw.jang fba3ef8626 fix(clinical): 병원 임상 모드에서 기기 연결로 갈 길이 없었다
"기기를 먼저 연결해 주세요" 라고 안내만 하고 정작 가는 길이 없었다. 연결이 이 모드의
첫 단계인데, 조작자는 홈까지 되돌아가 일반 흐름을 타고 다시 들어와야 했다.

## 복귀처를 불리언이 아니라 값으로
기기 연결 화면은 여러 곳에서 불려 온다. 원래는 `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 17:29:06 +09:00
dw.jang 53fd07c342 feat(clinical): 병원 임상 측정 모드 — 주파수×cycle 6조합 자동 순회
## 조작자가 하는 일은 하나다
간호사가 방광을 정해진 정도까지 채워 두면, 조작자는 **그 채움 정도만** 고르고
[측정 시작]을 누른다. 이후 주파수 2종 × cycle 3종 = 6조합을 앱이 자동으로 순회하며
각 조합마다 n회(기본 20) 측정하고 조합별 파일로 저장한다.

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 16:53:47 +09:00
dw.jang 26dc53b4eb chore(cleanup): YOLO/CameraX/OCR 세트 완전 제거 (Category B · 사용자 요청)
배경: 소변컵 사진 촬영 → YOLO 검출 → MLKit OCR 눈금 인식 기능 폐기.
현재 미사용. Piezo 방광 측정 flow 만 실사용.

완전 제거 (4 파일 + 1 asset):
  · measure/YoloDetector.kt (130 L · ONNX YOLO)
  · measure/SimpleMeasureService.kt (287 L · MLKit OCR + 3-zone)
  · measure/CCPosition.kt (8 L)
  · ui/views/monitoring/UrineCameraScreen.kt (285 L · CameraX 프리뷰)
  · assets/urinecup_best.onnx (~10.6 MB · APK 크기 절감)

Gradle deps 삭제 (build.gradle.kts):
  · com.microsoft.onnxruntime:onnxruntime-android:1.17.0
  · com.google.mlkit:text-recognition:16.0.1
  · androidx.camera:camera-{core,camera2,lifecycle,view}:1.3.4

AndroidManifest.xml 삭제:
  · CAMERA permission
  · android.hardware.camera uses-feature

부분 편집:
  · AppState.kt        — URINE_CAMERA enum value 삭제
  · MainActivity.kt    — import + back-handler branch + Crossfade branch 삭제
  · PiezoMonitoringView.kt — showUrineCamera state · 카메라 카드 (16L) ·
                             LaunchedEffect(showUrineCamera) 삭제
  · strings.xml × 2    — 카메라 전용 문자열 15개 삭제
                        (camera_input 은 CatheterizeSheet 사용 중이라 유지)

측정용 VoidingRecord.kt 는 유지 (CatheterizeSheet · VoidingDiaryView 등에서 사용).
okhttp 는 speech/HfVolumeExtractor 에서 사용 중이라 유지.

합계: 약 780 LOC + 10.6 MB APK 크기 절감.
빌드: BUILD SUCCESSFUL 30s (첫 시도 통과).
2026-08-12 15:49:27 +09:00
dw.jang 1ce19949c9 chore(cleanup): NIRS/VIVAMYO 세트 완전 제거 (Category A · 사용자 요청)
배경: NIRS 는 초기 iOS 앱에서 이식했으나 현재 완전 미사용. Piezo 만 실사용.

완전 제거 (6 파일):
  · managers/NirsManager.kt (94 L)
  · managers/NirsCalcEngine.kt (206 L)
  · models/NirsConstants.kt (49 L)
  · models/NirsData.kt (106 L · NirsMcjPacket · NirsMagResponse · NirsOxyResult)
  · ui/views/monitoring/VivamyoMonitoringView.kt (497 L)
  · ui/views/calibration/CalibrationView.kt (216 L) + calibration/ 디렉토리

부분 편집:
  · AppState.kt      — NirsManager import · NIRS_CALIBRATION/VIVAMYO_MONITORING enum ·
                       calibrationComplete() · sensor 분기 3곳 (goHome/enterDemoMode/
                       deviceConnected/backToMonitoring) 모두 PIEZO 로 단순화
  · MainActivity.kt  — CalibrationView/VivamyoMonitoringView import + Crossfade branch +
                       back-handler branch 삭제
  · BleManager.kt    — 콜백 4개 (onNirsPowerOn/SensorActivated/MagReceived/McjReceived) ·
                       sendRawWrite when-branch 4개 · NIRS Commands 섹션 5 함수 ·
                       RX branch 4개 삭제
  · MeasurementService.kt — performNirsMeasurement 제거 · performMeasurement 단순화 ·
                       stop() 콜백 정리 · SensorMode 유지 (센서 추가 대비)
  · SensorMode.kt    — NIRS enum value 삭제 · PIEZO 만 남음 · UserStorage 는
                       valueOf 실패 시 PIEZO fallback 이라 마이그레이션 불필요
  · SensorSelectView.kt — NIRS SensorCard 제거
  · strings.xml × 2  — sensor_nirs_desc · calibration_* 10개 · vivamyo_* 17개 삭제
                       (calibration_continue 는 PiezoPersonalization 사용 → 유지)

합계: 약 1,300 LOC 감소.
빌드: BUILD SUCCESSFUL 17s.
2026-08-12 15:38:59 +09:00
dw.jang 165df3c63d feat(telemetry): Phase B/C/D · ClinicalEventBus + emit (cloud-mvp 6856a7b 이식)
Phase D · telemetry/ClinicalEvent + ClinicalEventBus 신규 (demo-final path):
- sealed 계층 · schemaVersion · severity · isTransition · anonymized sessionId
- MutableSharedFlow (replay=0, buffer=64, DROP_OLDEST · fire-and-forget)
- PhiRedactor 없음 (demo-final) · TelemetryHash.sessionId (inline SHA-256) 사용

Phase A → BvEvent emit (AppState.updateLevelFromMeasurement):
- 매 측정마다 emit · urgency 전이 시 isTransition=true
- bvMethod 이름 · anonymized sessionId

Phase B/C → PostureEvent/GaitEvent emit (PiezoMonitoringView):
- composite posture 전이 시 PostureEvent (매 사이클 X · 트래픽 절약)
- walkingTransitioned 시 GaitEvent (severity=WARN on WALKING 진입)
- prevCompositePosture 상태 신규 추적

기존 SyncEvents/PostureEventLogger 와 병렬 · 서로 독립.

Refs: GUARDIAN-01 §3-2 이벤트 스키마 · §5 Phase A~D

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 17:22:48 +09:00
dw.jang 432127a667 feat(state): Phase A · 실측 경로 알람/이력 통합 (cloud-mvp deb7b17 이식)
Guardian 공유 이전 선행 작업 (GUARDIAN-01 §5 Phase A).

이전 버그: 데모 경로 (incrementLevel) 만 NotificationService · HistoryStore 호출.
        실측 (BLE) 경로는 bladderLevel = ...copy(currentLevel = level) 로 직접
        대입 → 알람 미발생 · 이력 미저장.

수정:
- AppState.updateLevelFromMeasurement(volumeMl, recordHistory) 신규 함수
  · bladderLevel 계산·갱신·storage 동기화
  · MeasurementRecord 저장 (recordHistory=true 시)
  · Urgency 변화 시 sendUrgencyNotification (중복 억제는 기존 로직 재사용)
- AppState.incrementLevel(): 데모 1단계 증가를 신규 함수로 위임
- PiezoMonitoringView 3곳 (L578/L1345/L1550) 직접 대입 → 신규 함수 호출
  · auto-scan 갱신: recordHistory=false (폭탄 방지)
  · SPOT trimmed 실측: recordHistory=true
  · dev-mode tap: recordHistory=true

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 16:58:06 +09:00
dw.jang 984fe2c939 feat(demo-final): cherry-pick — Firmware DFU + Battery dismiss + AppState fix (cloud-mvp 이식)
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>
2026-08-03 13:29:18 +09:00
dw.jang bcbc7b440c refactor: 데모 브랜치 패키지 통일 com.example.medilightv2android → com.medithings.vesiscan
사용자앱 (feature/tab-navigation) 과 패키지 이름 일치. namespace + applicationId
둘 다 com.medithings.vesiscan 로 변경 (기존 데모 앱은 재설치 필요).

## 변경 범위
- Kotlin 148 파일: package + import 문 (714 occurrences)
- 디렉토리 이동: com/example/medilightv2android → com/medithings/vesiscan
  (main, test, androidTest 각각)
- app/build.gradle.kts: namespace, applicationId
- docs/FLAVOR_DEMO_STABLE.md: 참조 갱신
- V41DetectorCH4Test: BvDispatchResult.methodChosen → method (dto field name fix)

## 주의
- applicationId 가 바뀌므로 기존 데모 앱 (com.example.medilightv2android.demo) 은
  Android 관점에서 다른 앱으로 취급 — 재설치 시 PIN/설정 초기화됨.
- Fresh install 권장. 기존 앱 (com.example...) 은 별도로 uninstall 필요.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 14:30:43 +09:00