CLINICAL: 흐름 재설계 — 한 측정 = 한 라벨 = 한 폴더
이전: 한 세션이 9 conditions × 6 steps를 모두 묶고 마지막에 zip
새로: 한 측정마다 라벨 입력 → 측정 → 자동 finalize → 다음 측정 라벨
사용자 요청: "session 들어가면 일단 BLE 연결을 하고, 연결 이후에 내가 어떤 자세로
어느 위치 측정할건지를 직접 적고 테스트를 완료한 뒤에 로그가 저장되도록"
변경:
- ClinicalSession 모델: 단일 측정 단위로 재정의 (subjectId/device/posture/step
/examiner/notes/startedAt/endedAt). folderName에 라벨 전체 + timestamp 포함:
"S001_A_SUPINE_ALIGN_0CM_2026-05-28_14-30-15"
- ClinicalSessionStore: 단순화. zip 제거. startMeasurement/endMeasurement.
inClinicalFlow boolean 추가 (DeviceScan/Back 라우팅용).
lastSubjectId/Examiner/Notes 기억 (다음 측정 시 자동 채움).
lastFinalizedFolder (UI에 "Saved: ..." 표시용).
- 3 로거: stepLogDir() → currentLogDir() 단순화. 한 폴더에 모든 파일.
- ClinicalHomeView 전면 재작성:
* BLE 연결 상태 카드 (미연결 시 Connect 버튼 → DeviceScan)
* 직전 측정 결과 표시 (lastFinalizedFolder)
* 라벨 입력 폼 (Subject + Device/Posture/Step segmented control + Examiner + Notes)
* 라벨 preview 실시간
* Start Measurement 버튼 (BLE 연결 + Subject ID 있을 때만 활성)
- AppState.deviceConnected: 임상 흐름이면 ClinicalHome 복귀 (Personalization 우회)
- MainActivity Back handler:
* DEVICE_SCAN Back → inClinicalFlow면 ClinicalHome
* PLACEMENT_GUIDE/PIEZO_MONITORING Back → 활성 세션이면 endMeasurement +
ClinicalHome 복귀 (폴더 finalize)
* CLINICAL_HOME Back → inClinicalFlow 해제 + HOME
데이터 구조 (한 측정당):
Downloads/VesiScan_Sessions/{subjectId}_{device}_{posture}_{step}_{timestamp}/
├─ meta.json (라벨 + 앱/펌웨어/디바이스 버전 + 시작/종료 시각)
├─ adc.csv
├─ placements.jsonl (alignment) or events.jsonl (BV)
└─ ble.log
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,10 +5,18 @@ import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* 임상 R&D 측정 세션 모델.
|
||||
* 임상 R&D 측정 — "한 측정 단위" 모델.
|
||||
*
|
||||
* 한 피험자의 한 측정 세션 = 3 devices × 3 postures × 6 steps = 54 measurements.
|
||||
* Dev 모드에서만 접근 가능. 세션 폴더로 모든 로그가 자동 라우팅됨.
|
||||
* 한 ClinicalSession = 한 라벨 = 한 폴더 = 한 측정 사이클.
|
||||
* 사용자가 BLE 연결 후 매번 라벨(Subject/Device/Posture/Step)을 입력하고
|
||||
* 측정을 진행하면, 측정 종료 시점에 폴더가 finalize됨.
|
||||
*
|
||||
* 데이터 구조 (한 측정당):
|
||||
* Downloads/VesiScan_Sessions/{folderName}/
|
||||
* ├─ meta.json
|
||||
* ├─ adc.csv
|
||||
* ├─ placements.jsonl (alignment) or events.jsonl (BV)
|
||||
* └─ ble.log
|
||||
*/
|
||||
|
||||
enum class ClinicalDevice(val label: String) {
|
||||
@@ -20,63 +28,36 @@ enum class ClinicalPosture(val label: String) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 한 condition 내 측정 단계.
|
||||
* 측정 단계.
|
||||
* - ALIGN_0CM ~ ALIGN_4CM: 정렬모드 알고리즘 개발용 (치골 위 0~4cm)
|
||||
* - BV: cradle 부착 후 본측정 (방광 추정 알고리즘 개발용)
|
||||
*/
|
||||
enum class ClinicalStep(val label: String, val folder: String, val isAlignment: Boolean) {
|
||||
ALIGN_0CM("0 cm", "align_0cm", true),
|
||||
ALIGN_1CM("1 cm", "align_1cm", true),
|
||||
ALIGN_2CM("2 cm", "align_2cm", true),
|
||||
ALIGN_3CM("3 cm", "align_3cm", true),
|
||||
ALIGN_4CM("4 cm", "align_4cm", true),
|
||||
BV("BV", "bv", false)
|
||||
enum class ClinicalStep(val label: String, val isAlignment: Boolean) {
|
||||
ALIGN_0CM("0 cm", true),
|
||||
ALIGN_1CM("1 cm", true),
|
||||
ALIGN_2CM("2 cm", true),
|
||||
ALIGN_3CM("3 cm", true),
|
||||
ALIGN_4CM("4 cm", true),
|
||||
BV("BV", false)
|
||||
}
|
||||
|
||||
data class ClinicalSession(
|
||||
val sessionId: String, // "S001" — 사용자 입력 라벨 (익명)
|
||||
val subjectId: String, // "S001"
|
||||
val device: ClinicalDevice,
|
||||
val posture: ClinicalPosture,
|
||||
val step: ClinicalStep,
|
||||
val examiner: String = "",
|
||||
val notes: String = "",
|
||||
val startedAt: Long = System.currentTimeMillis(),
|
||||
var endedAt: Long? = null,
|
||||
var currentDevice: ClinicalDevice? = null,
|
||||
var currentPosture: ClinicalPosture? = null,
|
||||
var currentStep: ClinicalStep? = null,
|
||||
val completedKeys: MutableSet<String> = mutableSetOf()
|
||||
var endedAt: Long? = null
|
||||
) {
|
||||
private val folderFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
|
||||
|
||||
/** Downloads/VesiScan_Sessions/{folderName}/ 의 폴더명 */
|
||||
/** "S001_A_SUPINE_ALIGN_0CM_2026-05-28_14-30-15" */
|
||||
val folderName: String
|
||||
get() = "${sessionId}_${folderFmt.format(Date(startedAt))}"
|
||||
get() = "${subjectId}_${device.name}_${posture.name}_${step.name}_${folderFmt.format(Date(startedAt))}"
|
||||
|
||||
/** "A_SUPINE_ALIGN_0CM" — 라벨/완료 추적용 키 */
|
||||
fun stepKey(d: ClinicalDevice, p: ClinicalPosture, s: ClinicalStep): String =
|
||||
"${d.name}_${p.name}_${s.name}"
|
||||
|
||||
fun currentStepKey(): String? {
|
||||
val d = currentDevice ?: return null
|
||||
val p = currentPosture ?: return null
|
||||
val s = currentStep ?: return null
|
||||
return stepKey(d, p, s)
|
||||
}
|
||||
|
||||
fun markCompleted(d: ClinicalDevice, p: ClinicalPosture, s: ClinicalStep) {
|
||||
completedKeys.add(stepKey(d, p, s))
|
||||
}
|
||||
|
||||
fun markCurrentCompleted() {
|
||||
currentStepKey()?.let { completedKeys.add(it) }
|
||||
}
|
||||
|
||||
fun isCompleted(d: ClinicalDevice, p: ClinicalPosture, s: ClinicalStep): Boolean =
|
||||
stepKey(d, p, s) in completedKeys
|
||||
|
||||
fun conditionCompletedCount(d: ClinicalDevice, p: ClinicalPosture): Int =
|
||||
ClinicalStep.values().count { isCompleted(d, p, it) }
|
||||
|
||||
val totalSteps: Int =
|
||||
ClinicalDevice.values().size * ClinicalPosture.values().size * ClinicalStep.values().size
|
||||
|
||||
val completedCount: Int get() = completedKeys.size
|
||||
/** clinical_label 컬럼/필드 값 (라벨링 자동) */
|
||||
val label: String
|
||||
get() = "${subjectId}_${device.name}_${posture.name}_${step.name}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user