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
@@ -44,14 +44,22 @@ data class ClinicalSession(
val step: ClinicalStep,
val examiner: String = "",
val notes: String = "",
// 임상 시험 정보 (vesiscan_test Section 2-1 프로토콜과 매칭)
val subjectId: String = "", // 피험자 ID (예: "S001")
val trueVolumeMl: Double? = null, // 외부 초음파 측정기 참값 (mL)
val abdomenThicknessMm: Double? = null, // 복부 두께 측정기 참값 (mm)
val startedAt: Long = System.currentTimeMillis(),
var endedAt: Long? = null
) {
private val folderFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
/** "VBT26050202_SUPINE_ALIGN_0CM_2026-05-28_14-30-15" */
/** "S001_VBT26050202_SUPINE_ALIGN_0CM_2026-05-28_14-30-15"
* subjectId 비어있으면 기존 형식 유지 (deviceName_posture_step_timestamp). */
val folderName: String
get() = "${deviceName}_${posture.name}_${step.name}_${folderFmt.format(Date(startedAt))}"
get() = buildString {
if (subjectId.isNotBlank()) { append(subjectId); append('_') }
append("${deviceName}_${posture.name}_${step.name}_${folderFmt.format(Date(startedAt))}")
}
/** clinical_label 컬럼/필드 값 (라벨링 자동) */
val label: String