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>
271 lines
9.5 KiB
Kotlin
271 lines
9.5 KiB
Kotlin
package com.example.medilightv2android
|
|
|
|
import android.content.Context
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableIntStateOf
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.setValue
|
|
import com.example.medilightv2android.ble.BleManager
|
|
import com.example.medilightv2android.managers.NirsManager
|
|
import com.example.medilightv2android.models.*
|
|
import com.example.medilightv2android.services.*
|
|
|
|
enum class AppScreen {
|
|
ONBOARDING, CONSENT, REGISTRATION, HOME, SENSOR_SELECT, DEVICE_SCAN,
|
|
NIRS_CALIBRATION, PIEZO_PERSONALIZATION,
|
|
PLACEMENT_GUIDE, PIN_SETUP, PIN_ENTRY,
|
|
VIVAMYO_MONITORING, PIEZO_MONITORING, MEASUREMENT_HISTORY,
|
|
VOIDING_DIARY, URINE_CAMERA, REMINDER_SETTINGS,
|
|
CLINICAL_HOME, // ★ Dev 모드 한정 — 임상 R&D 측정 세션 화면 (입력 폼)
|
|
CLINICAL_LIVE // ★ Dev 모드 한정 — 6채널 라이브 시각화 + Capture
|
|
}
|
|
|
|
class AppState(private val context: Context) {
|
|
var currentScreen by mutableStateOf(AppScreen.ONBOARDING)
|
|
var user by mutableStateOf(User())
|
|
var sensorMode by mutableStateOf(SensorMode.PIEZO)
|
|
var bladderLevel by mutableStateOf(BladderLevel())
|
|
var isDeviceConnected by mutableStateOf(false)
|
|
var isDemoMode by mutableStateOf(false)
|
|
var isDevMode by mutableStateOf(false)
|
|
var piezoSettings by mutableStateOf(PiezoSettings())
|
|
var catheterCount = mutableIntStateOf(0)
|
|
// Placement 진입 경로 분기용 — Monitoring에서 재진입 시 true, Personalization에서 진입 시 false
|
|
var placementFromMonitoring by mutableStateOf(false)
|
|
|
|
private val storage = UserStorage.getInstance(context)
|
|
private val historyStore = MeasurementHistoryStore.getInstance(context)
|
|
private val notificationService = NotificationService.getInstance(context)
|
|
private val bleManager = BleManager.getInstance(context)
|
|
private val reminderService = ReminderService.getInstance(context)
|
|
|
|
init {
|
|
loadState()
|
|
}
|
|
|
|
fun loadState() {
|
|
user = storage.user
|
|
sensorMode = storage.sensorMode
|
|
bladderLevel = bladderLevel.copy(
|
|
currentLevel = storage.bladderLevel,
|
|
catheterizationThreshold = storage.catheterizationLevel
|
|
)
|
|
piezoSettings = storage.piezoSettings
|
|
catheterCount.intValue = storage.catheterCount
|
|
|
|
currentScreen = when {
|
|
!storage.hasCompletedOnboarding -> AppScreen.ONBOARDING
|
|
!storage.isRegistered -> AppScreen.REGISTRATION
|
|
!storage.hasPin -> AppScreen.PIN_SETUP
|
|
else -> AppScreen.PIN_ENTRY
|
|
}
|
|
}
|
|
|
|
fun completeOnboarding() {
|
|
storage.hasCompletedOnboarding = true
|
|
storage.hasConsented = true
|
|
currentScreen = AppScreen.REGISTRATION
|
|
}
|
|
|
|
fun completeConsent() {
|
|
storage.hasConsented = true
|
|
storage.consentTimestamp = System.currentTimeMillis()
|
|
currentScreen = AppScreen.REGISTRATION
|
|
}
|
|
|
|
fun completeRegistration() {
|
|
storage.user = user
|
|
storage.isRegistered = true
|
|
currentScreen = AppScreen.PIN_SETUP
|
|
}
|
|
|
|
fun goHome() {
|
|
NirsManager.instance.stopStreaming()
|
|
NirsManager.instance.reset()
|
|
// 회의 결정: 실제 BLE 연결은 HOME에서도 유지. 다음 Start 시 설정/scan skip하고
|
|
// 바로 PiezoMonitoring으로 진입할 수 있도록.
|
|
// Demo 모드는 가짜 연결이므로 isDeviceConnected reset.
|
|
if (isDemoMode) isDeviceConnected = false
|
|
isDemoMode = false
|
|
currentScreen = AppScreen.HOME
|
|
}
|
|
|
|
fun enterDemoMode() {
|
|
isDemoMode = true
|
|
isDeviceConnected = true
|
|
currentScreen = if (sensorMode == SensorMode.NIRS) {
|
|
AppScreen.VIVAMYO_MONITORING
|
|
} else {
|
|
AppScreen.PIEZO_MONITORING
|
|
}
|
|
}
|
|
|
|
fun startFromHome() {
|
|
sensorMode = SensorMode.PIEZO
|
|
storage.sensorMode = SensorMode.PIEZO
|
|
// 회의 결정: 이미 연결된 상태이면 설정/Placement skip하고 바로 도넛차트.
|
|
// 연결되지 않은 상태에서만 DeviceScan부터 일련의 과정 진행.
|
|
currentScreen = if (isDeviceConnected) AppScreen.PIEZO_MONITORING else AppScreen.DEVICE_SCAN
|
|
}
|
|
|
|
fun selectSensor(mode: SensorMode) {
|
|
sensorMode = mode
|
|
storage.sensorMode = mode
|
|
currentScreen = AppScreen.DEVICE_SCAN
|
|
}
|
|
|
|
fun backToSensorSelect() {
|
|
currentScreen = AppScreen.HOME
|
|
}
|
|
|
|
fun deviceConnected() {
|
|
isDeviceConnected = true
|
|
// 임상 모드에서 BLE 연결 시 ClinicalHome으로 복귀 (Personalization 우회)
|
|
if (com.example.medilightv2android.services.ClinicalSessionStore.inClinicalFlow) {
|
|
currentScreen = AppScreen.CLINICAL_HOME
|
|
return
|
|
}
|
|
currentScreen = if (sensorMode == SensorMode.NIRS) {
|
|
AppScreen.NIRS_CALIBRATION
|
|
} else {
|
|
AppScreen.PIEZO_PERSONALIZATION
|
|
}
|
|
}
|
|
|
|
fun calibrationComplete() {
|
|
currentScreen = AppScreen.VIVAMYO_MONITORING
|
|
}
|
|
|
|
fun personalizationComplete() {
|
|
storage.piezoSettings = piezoSettings
|
|
placementFromMonitoring = false
|
|
// 회의 #8: AutoScanInterval을 Personalization 페이지에 통합 — 별도 화면 폐기.
|
|
currentScreen = AppScreen.PLACEMENT_GUIDE
|
|
}
|
|
|
|
/** Persist piezoSettings (called from in-session settings save). */
|
|
fun savePiezoSettingsNow() {
|
|
// Re-assign to trigger the mutableStateOf delegate so any composables
|
|
// observing piezoSettings recompose with the latest value, then persist
|
|
// to disk so the change survives process death.
|
|
piezoSettings = piezoSettings.copy()
|
|
storage.piezoSettings = piezoSettings
|
|
}
|
|
|
|
fun placementComplete() {
|
|
bladderLevel = BladderLevel(currentLevel = 0, catheterizationThreshold = bladderLevel.catheterizationThreshold)
|
|
storage.bladderLevel = 0
|
|
placementFromMonitoring = false
|
|
currentScreen = AppScreen.PIEZO_MONITORING
|
|
}
|
|
|
|
fun enterPlacementFromMonitoring() {
|
|
placementFromMonitoring = true
|
|
currentScreen = AppScreen.PLACEMENT_GUIDE
|
|
}
|
|
|
|
/** Clinical Mode alignment session 진입 — PlacementGuide 로 이동.
|
|
* ClinicalSessionStore.startMeasurement(mode=ALIGNMENT) 가 먼저 호출돼 있어야 함. */
|
|
fun enterAlignmentSession() {
|
|
placementFromMonitoring = false
|
|
currentScreen = AppScreen.PLACEMENT_GUIDE
|
|
}
|
|
|
|
fun backFromPlacementGuide() {
|
|
// 회의 #8: AutoScan을 Personalization으로 통합했으므로 Placement Back은
|
|
// 바로 Personalization으로 (이전 AUTO_SCAN_SETUP 단계 폐기).
|
|
currentScreen = if (placementFromMonitoring) AppScreen.PIEZO_MONITORING
|
|
else AppScreen.PIEZO_PERSONALIZATION
|
|
placementFromMonitoring = false
|
|
}
|
|
|
|
fun pinSet(pin: String) {
|
|
storage.pin = pin
|
|
storage.hasPin = true
|
|
currentScreen = AppScreen.HOME
|
|
}
|
|
|
|
fun pinVerified() {
|
|
currentScreen = AppScreen.HOME
|
|
}
|
|
|
|
fun incrementLevel() {
|
|
val previousUrgency = bladderLevel.urgency
|
|
val newLevel = bladderLevel.copy()
|
|
newLevel.increment()
|
|
bladderLevel = newLevel
|
|
storage.bladderLevel = bladderLevel.currentLevel
|
|
|
|
val record = MeasurementRecord(
|
|
eventType = MeasurementEventType.MEASUREMENT,
|
|
level = bladderLevel.currentLevel,
|
|
estimatedVolume = estimatedVolume,
|
|
urgencyLevel = bladderLevel.urgency,
|
|
fillPercentage = fillPercentage
|
|
)
|
|
historyStore.addRecord(record)
|
|
|
|
val newUrgency = bladderLevel.urgency
|
|
if (newUrgency != previousUrgency) {
|
|
notificationService.sendUrgencyNotification(newUrgency, estimatedVolume)
|
|
}
|
|
}
|
|
|
|
fun resetLevel() {
|
|
val record = MeasurementRecord(
|
|
eventType = MeasurementEventType.CATHETERIZATION,
|
|
level = bladderLevel.currentLevel,
|
|
estimatedVolume = estimatedVolume,
|
|
urgencyLevel = bladderLevel.urgency,
|
|
fillPercentage = fillPercentage
|
|
)
|
|
historyStore.addRecord(record)
|
|
|
|
val newLevel = bladderLevel.copy()
|
|
newLevel.reset()
|
|
bladderLevel = newLevel
|
|
storage.bladderLevel = 0
|
|
notificationService.cancelAll()
|
|
reminderService.reschedule()
|
|
|
|
if (catheterCount.intValue > 0) {
|
|
catheterCount.intValue--
|
|
storage.catheterCount = catheterCount.intValue
|
|
}
|
|
}
|
|
|
|
fun showHistory() {
|
|
currentScreen = AppScreen.MEASUREMENT_HISTORY
|
|
}
|
|
|
|
fun backToMonitoring() {
|
|
currentScreen = if (sensorMode == SensorMode.NIRS) AppScreen.VIVAMYO_MONITORING else AppScreen.PIEZO_MONITORING
|
|
}
|
|
|
|
fun addCatheterCount(amount: Int = 1) {
|
|
catheterCount.intValue += amount
|
|
storage.catheterCount = catheterCount.intValue
|
|
}
|
|
|
|
fun updateCatheterizationThreshold(threshold: Int) {
|
|
bladderLevel = bladderLevel.copy(catheterizationThreshold = threshold)
|
|
storage.catheterizationLevel = threshold
|
|
}
|
|
|
|
fun savePiezoSettings() {
|
|
storage.piezoSettings = piezoSettings
|
|
}
|
|
|
|
val estimatedVolume: Int
|
|
get() {
|
|
if (BladderLevel.MAX_LEVEL <= 0) return 0
|
|
return (bladderLevel.currentLevel.toDouble() / BladderLevel.MAX_LEVEL * piezoSettings.maxVolume).toInt()
|
|
}
|
|
|
|
val fillPercentage: Double
|
|
get() {
|
|
if (BladderLevel.MAX_LEVEL <= 0) return 0.0
|
|
return bladderLevel.currentLevel.toDouble() / BladderLevel.MAX_LEVEL * 100.0
|
|
}
|
|
}
|