feat: Clinical Mode 에 Sensor Alignment 세션 추가 (demo-final, V1 전용)
새 흐름:
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>
This commit is contained in:
@@ -12,12 +12,20 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/** 한 측정 사이클 = mtb 응답 1회 = 6채널 piezo + 15 IMU 샘플. */
|
||||
/** 한 측정 사이클 = mtb 응답 1회 = 6채널 piezo + 15 IMU 샘플.
|
||||
* alignment 모드일 때 alignXxx 필드들이 채워짐 (V1 advisor 출력). */
|
||||
data class MeasurementCycle(
|
||||
val cycle: Int,
|
||||
val timestampMs: Long,
|
||||
val piezoChannels: Map<Int, List<Int>>, // channel idx → samples
|
||||
val imuSamples: List<ImuSample>
|
||||
val imuSamples: List<ImuSample>,
|
||||
// Alignment mode metadata (V1 placementPhase, detected channels, urineLens)
|
||||
val alignPhase: String = "", // VERTICAL / LATERAL / GREEN / ""
|
||||
val alignScore: Int = 0, // placementScore
|
||||
val alignDetected: List<Boolean> = emptyList(), // 6개 채널 검출 여부
|
||||
val alignUrineLens: List<Int> = emptyList(), // 6개 채널 urineLen
|
||||
val alignHint: String = "", // directionHint
|
||||
val alignIcon: String = "", // directionIcon
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -92,6 +100,33 @@ object ClinicalSessionStore {
|
||||
))
|
||||
}
|
||||
|
||||
/** Alignment 모드 frame 누적 — V1 advisor 출력 포함 (phase / score / detected / urineLens / hint). */
|
||||
fun addAlignmentFrame(
|
||||
piezo: List<PiezoChannelData>,
|
||||
imu: List<ImuSample>,
|
||||
alignPhase: String,
|
||||
alignScore: Int,
|
||||
alignDetected: List<Boolean>,
|
||||
alignUrineLens: List<Int>,
|
||||
alignHint: String,
|
||||
alignIcon: String,
|
||||
) {
|
||||
if (currentSession == null) return
|
||||
cyclesBuffer.add(MeasurementCycle(
|
||||
cycle = cyclesBuffer.size,
|
||||
timestampMs = System.currentTimeMillis(),
|
||||
piezoChannels = piezo.sortedBy { it.channel }
|
||||
.associate { it.channel to it.buffer.map { s -> s.toInt() } },
|
||||
imuSamples = imu.toList(),
|
||||
alignPhase = alignPhase,
|
||||
alignScore = alignScore,
|
||||
alignDetected = alignDetected,
|
||||
alignUrineLens = alignUrineLens,
|
||||
alignHint = alignHint,
|
||||
alignIcon = alignIcon,
|
||||
))
|
||||
}
|
||||
|
||||
val cycleCount: Int get() = cyclesBuffer.size
|
||||
|
||||
/** 현재 측정의 폴더 (= 모든 로거의 저장 디렉토리). */
|
||||
@@ -196,8 +231,13 @@ object ClinicalSessionStore {
|
||||
val root = JSONObject()
|
||||
|
||||
// meta: writeMeta()와 동일 내용 포함 (단독 파일로 자족 가능)
|
||||
// alignment 모드 → dataType=002, 그 외 = 001 (기존 measurement)
|
||||
val effectiveDataType = if (s.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT)
|
||||
"002" else DATA_TYPE
|
||||
val meta = JSONObject().apply {
|
||||
put("data_type", DATA_TYPE)
|
||||
put("data_type", effectiveDataType)
|
||||
put("session_mode", s.mode.name)
|
||||
if (s.alignmentAlgo.isNotBlank()) put("alignment_algo", s.alignmentAlgo)
|
||||
put("subject_type", s.subjectType.name)
|
||||
if (s.subject.isNotBlank()) put("subject", s.subject)
|
||||
put("device_name", s.deviceName)
|
||||
@@ -252,6 +292,15 @@ object ClinicalSessionStore {
|
||||
put("timestamp_ms", c.timestampMs)
|
||||
put("piezo", piezo)
|
||||
put("imu", imuArr)
|
||||
// Alignment metadata (alignment mode 일 때만 채워짐)
|
||||
if (c.alignPhase.isNotBlank()) {
|
||||
put("align_phase", c.alignPhase)
|
||||
put("align_score", c.alignScore)
|
||||
put("align_hint", c.alignHint)
|
||||
put("align_icon", c.alignIcon)
|
||||
put("align_detected", JSONArray(c.alignDetected))
|
||||
put("align_urine_lens", JSONArray(c.alignUrineLens))
|
||||
}
|
||||
})
|
||||
}
|
||||
root.put("cycles", cyclesArr)
|
||||
|
||||
Reference in New Issue
Block a user