429964571f
새 흐름:
ClinicalHome → BLE 연결 + labdb 활성 확인
→ [Sensor Alignment] 버튼 (우측, 보라색)
→ PlacementGuide (alignment session 모드, V1 advisor)
→ 매 cycle phase/score/detected/urineLens/hint 누적
→ [Complete Alignment] 버튼
→ measurement.json (data_type='002') + labdb 자동 업로드
→ ClinicalHome 복귀
Live 6ch 측정은 기존대로:
ClinicalHome → [Live 6ch] → ClinicalLive → measurement.json (data_type='001')
models/ClinicalSession.kt:
+ SessionMode { ALIGNMENT, MEASUREMENT } enum
+ ClinicalSession.mode (default = MEASUREMENT, 기존 동작 유지)
+ ClinicalSession.alignmentAlgo ("V1" demo-final 에서)
+ folderName 에 mode == ALIGNMENT 시 "_ALIGNMODE_" 접미사
services/ClinicalSessionStore.kt:
+ MeasurementCycle 에 alignment 필드 6개 (phase/score/detected/urineLens/hint/icon)
+ addAlignmentFrame(piezo, imu, ...V1 advisor state) 새 메서드
+ writeMeasurementJson:
mode==ALIGNMENT → data_type='002' (R&D alignment)
그 외 = '001' (measurement)
각 cycle JSON 에 align_phase / align_score / align_detected /
align_urine_lens / align_hint / align_icon 첨부
AppState.kt:
+ enterAlignmentSession()
ui/views/clinical/ClinicalHomeView.kt:
+ Live 6ch + Sensor Alignment 2-button Row 로 교체
+ Sensor Alignment: BLE + labdb active 두 조건 모두 OK 시만 활성
+ 비활성 시 안내 텍스트
ui/views/monitoring/PlacementGuideView.kt:
+ 매 cycle ClinicalSessionStore.addAlignmentFrame 호출
(clinical alignment 모드 시) — phase=placementPhase.name, score, detected,
urineLens, hint, icon
+ Start Scanning 버튼이 clinical alignment 모드에선 "Complete Alignment" 로 전환
→ endMeasurement() → CLINICAL_HOME 복귀 (자동 labdb upload)
→ enabled 조건도 항상 활성 (사용자 직접 종료)
labdb 업로드: 기존 LabdbUploader 경로 그대로 — meta.data_type 으로 002/001 구분.
영향:
- 기존 Live 6ch flow: 영향 0 (mode=MEASUREMENT default)
- 기존 PlacementGuide UX: 영향 0 (clinical alignment 모드 아닐 때 분기 X)
- 신규: Clinical 내 alignment-only 세션 채집/업로드 가능
feature/tab-navigation 에 V2 advisor 버전이 별도 적용됨 (commit 173ff4d 까지).
demo-final 은 V1 advisor 만 가능 — V2/Method D/RollingAligner 코드 없음.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
90 lines
3.4 KiB
Kotlin
90 lines
3.4 KiB
Kotlin
package com.example.medilightv2android.models
|
|
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
|
|
/**
|
|
* 임상 R&D 측정 — "한 측정 단위" 모델.
|
|
*
|
|
* 한 ClinicalSession = 한 라벨 = 한 폴더 = 한 측정 사이클.
|
|
* 사용자가 BLE 연결 후 매번 자세/위치를 입력하고 측정 진행 → Back 시 폴더 finalize.
|
|
*
|
|
* 디바이스 식별자는 BLE 연결된 기기 이름(예: VBT26050202)을 자동으로 사용.
|
|
*
|
|
* 데이터 구조 (한 측정당):
|
|
* Downloads/VesiScan_Sessions/{folderName}/
|
|
* ├─ meta.json
|
|
* ├─ adc.csv
|
|
* ├─ placements.jsonl (alignment) or events.jsonl (BV)
|
|
* └─ ble.log
|
|
*/
|
|
|
|
enum class ClinicalPosture(val label: String) {
|
|
SUPINE("Supine"), SITTING("Sitting"), STANDING("Standing")
|
|
}
|
|
|
|
/** 측정 대상 분류 — 회의 결정 (subject ID 자유 입력 대체). */
|
|
enum class SubjectType(val label: String) {
|
|
PHANTOM("Phantom"), HUMAN("Human"), ETC("ETC")
|
|
}
|
|
|
|
/**
|
|
* 측정 단계 — 회의 결정으로 BV 제거. 정렬모드 알고리즘 개발용 5단계만.
|
|
* - ALIGN_0CM ~ ALIGN_4CM: 기기 아래 끝 기준 치골 위 0~4cm
|
|
*/
|
|
enum class ClinicalStep(val label: String, val description: String, val isAlignment: Boolean) {
|
|
ALIGN_0CM("0 cm", "기기 아래 끝이 치골 바로 위", true),
|
|
ALIGN_1CM("1 cm", "치골 위로 1 cm", true),
|
|
ALIGN_2CM("2 cm", "치골 위로 2 cm", true),
|
|
ALIGN_3CM("3 cm", "치골 위로 3 cm", true),
|
|
ALIGN_4CM("4 cm", "치골 위로 4 cm", true),
|
|
}
|
|
|
|
/**
|
|
* 한 ClinicalSession 의 데이터 수집 모드.
|
|
* - ALIGNMENT : PlacementGuide(Sensor Alignment) 에서 V1 advisor 프레임 + 라벨 누적
|
|
* → labdb 업로드 (dataType="002")
|
|
* - MEASUREMENT: ClinicalLive 에서 6ch 실시간 측정 누적 (기존 동작)
|
|
* → labdb 업로드 (dataType="001")
|
|
*/
|
|
enum class SessionMode { ALIGNMENT, MEASUREMENT }
|
|
|
|
data class ClinicalSession(
|
|
val deviceName: String, // BLE 기기 이름 (예: "VBT26050202")
|
|
val posture: ClinicalPosture,
|
|
val step: ClinicalStep,
|
|
val examiner: String = "",
|
|
val notes: String = "",
|
|
val subjectType: SubjectType = SubjectType.PHANTOM,
|
|
val subject: String = "",
|
|
val trueVolumeMl: Double? = null, // 외부 초음파 측정기 참값 (mL)
|
|
val abdomenThicknessMm: Double? = null, // 복부 두께 측정기 참값 (mm)
|
|
val mode: SessionMode = SessionMode.MEASUREMENT, // 신규
|
|
val alignmentAlgo: String = "", // demo-final: V1 only
|
|
val startedAt: Long = System.currentTimeMillis(),
|
|
var endedAt: Long? = null
|
|
) {
|
|
private val folderFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
|
|
|
|
/** mode == ALIGNMENT 시 "_ALIGNMODE_" 접미사 (Live 6ch 세션과 폴더 분리). */
|
|
val folderName: String
|
|
get() = buildString {
|
|
append(subjectType.name)
|
|
if (subject.isNotBlank()) {
|
|
append('-')
|
|
append(subject.replace(Regex("[^A-Za-z0-9_-]"), "-"))
|
|
}
|
|
append('_')
|
|
append(deviceName); append('_')
|
|
append(posture.name); append('_')
|
|
append(step.name); append('_')
|
|
if (mode == SessionMode.ALIGNMENT) append("ALIGNMODE_")
|
|
append(folderFmt.format(Date(startedAt)))
|
|
}
|
|
|
|
/** clinical_label 컬럼/필드 값 (라벨링 자동) */
|
|
val label: String
|
|
get() = "${deviceName}_${posture.name}_${step.name}"
|
|
}
|