feat: 임상 Live 6채널 시각화 화면 (ClinicalLiveView)

vesiscan_test Section 2 프로토콜 패턴을 안드로이드 앱 안에서 직접 구현.
실제 몸에 센서 붙인 채 6채널 실시간 그래프 보면서 측정 + 저장 가능.

신규 화면 ClinicalLiveView:
- 600ms 간격 mtb? 자동 송신 (Piezo 6ch + IMU 동시)
- 2×3 grid 라이브 차트 (raw ADC waveform, CH0~CH5 색상별)
  · y축 0~4095 (12-bit ADC) 고정 + 가로 grid 4분할
  · 각 채널 peak 값 표시
- IMU 카드 — ImuAnalysis 기반 자세 + tilt + shake (흔들림 시 빨강 경고)
- 상단: 세션 라벨 + S=Subject TV=TrueVol Ab=Abdomen 한 줄 + #cycle ✓captured
- 컨트롤: Pause/Resume · Capture (수동 저장) · Auto Capture 토글 · End Session

ClinicalSession 모델 확장:
- subjectId / trueVolumeMl / abdomenThicknessMm 필드 추가
- folderName 에 subjectId prefix (예: "S001_VBT26050202_SUPINE_ALIGN_0CM_...")
- meta.json 에 subject_id / true_volume_ml / abdomen_thickness_mm 자동 기록

ClinicalSessionStore:
- lastSubjectId / lastTrueVolumeMl / lastAbdomenThicknessMm 기억 (다음 측정 자동 채움)

ClinicalHomeView 입력 폼 확장:
- Subject ID 텍스트필드
- True Volume (mL) + Abdomen (mm) 2-컬럼 숫자 입력
- Start Live Measurement 버튼 → CLINICAL_LIVE 화면으로 직행
  (이전: alignment/BV step에 따라 PlacementGuide/PiezoMonitoring 분기)

AppState/MainActivity:
- AppScreen.CLINICAL_LIVE 추가
- MainActivity Back: LIVE 시 endMeasurement + ClinicalHome 복귀

데이터 흐름:
  Start Live → ClinicalLiveView 진입 → mtb? 600ms loop
    → piezoCollector → 6채널 buffer (자동 또는 수동 capture)
    → AdcCsvLogger.log → adc.csv 누적
    → imuCollector → 15 IMU samples
    → ImuCsvLogger.log → imu.csv 누적
  End/Back → endMeasurement → meta.json finalize → ClinicalHome 복귀

장점:
- 검사자가 실제 시연 중 6채널 신호 품질 즉시 확인
- Auto Capture로 매 사이클 자동 저장 (Pause로 일시 멈춤)
- subject_id 기반 폴더명으로 피험자별 grouping 자연스럽게

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 11:17:55 +09:00
parent ec3be6fa5d
commit 38fa93c8b2
6 changed files with 430 additions and 9 deletions
@@ -34,14 +34,20 @@ object ClinicalSessionStore {
var lastFinalizedFolder: File? = null
private set
/** 다음 측정 시 기본값으로 자동 채울 사용자 입력 (examiner/notes) */
/** 다음 측정 시 기본값으로 자동 채울 사용자 입력 — 같은 피험자/검사자 여러 측정 시 편의 */
var lastExaminer: String = ""
var lastNotes: String = ""
var lastSubjectId: String = ""
var lastTrueVolumeMl: String = "" // 문자열로 보관 (입력 폼과 동일 형식)
var lastAbdomenThicknessMm: String = ""
fun startMeasurement(context: Context, session: ClinicalSession) {
currentSession = session
lastExaminer = session.examiner
lastNotes = session.notes
lastSubjectId = session.subjectId
lastTrueVolumeMl = session.trueVolumeMl?.toString() ?: ""
lastAbdomenThicknessMm = session.abdomenThicknessMm?.toString() ?: ""
currentLogDir()?.mkdirs()
writeMeta(context)
}
@@ -68,6 +74,10 @@ object ClinicalSessionStore {
put("examiner", s.examiner)
put("notes", s.notes)
put("label", s.label)
// 임상 시험 참값 (외부 측정기, vesiscan_test 프로토콜)
if (s.subjectId.isNotBlank()) put("subject_id", s.subjectId)
s.trueVolumeMl?.let { put("true_volume_ml", it) }
s.abdomenThicknessMm?.let { put("abdomen_thickness_mm", it) }
put("started_at", s.startedAt)
put("started_at_iso",
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(Date(s.startedAt)))