diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index 6a84c6e..867afc3 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -132,7 +132,10 @@ fun PlacementGuideView(appState: AppState) { if (isClinicalAlign) com.example.medilightv2android.managers.AlignmentAlgo.V2 else com.example.medilightv2android.managers.AlignmentAlgo.V1 v2Aligner.reset() - isContinuousMode = false + // Clinical alignment session 진입 시 — 자동으로 scan 시작 (Step 0 baseline 캡쳐). + // waitingForStart 는 true 유지 — 사용자는 여전히 Start 눌러 알고리즘 시작 결정. + // baseline cycles 는 measurement.json 에 phase="STEP_0_BASELINE" 로 누적됨. + isContinuousMode = isClinicalAlign waitingForStart = true placementPhase = PlacementPhase.VERTICAL isLocked = false @@ -245,7 +248,19 @@ fun PlacementGuideView(appState: AppState) { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - IconButton(onClick = { appState.backFromPlacementGuide() }) { + IconButton(onClick = { + isContinuousMode = false + // Clinical alignment session 진입한 경우 — abort 후 Clinical Home 으로. + // (PiezoMonitoring / Personalization 으로 떨어지지 않게) + val curSess = com.example.medilightv2android.services.ClinicalSessionStore.currentSession + if (curSess != null && curSess.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT) { + com.example.medilightv2android.services.ClinicalSessionStore.discardAndDeleteFolder() + com.example.medilightv2android.services.ClinicalSessionStore.inClinicalFlow = true + appState.currentScreen = AppScreen.CLINICAL_HOME + } else { + appState.backFromPlacementGuide() + } + }) { Icon( Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", @@ -827,6 +842,13 @@ fun PlacementGuideView(appState: AppState) { } else if (com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo == com.example.medilightv2android.managers.AlignmentAlgo.V2) { // ═══ V2 alignment (alignment.py 포팅, method_d 기반) ═══ + // Clinical alignment session + waitingForStart 일 땐 알고리즘 skip — + // baseline cycles 만 누적 (Start 누르기 전 자동 phase 진행 차단). + if (waitingForStart) { + directionHint = "Place sensor above pubic bone, then press Start" + directionIcon = "" + placementScore = 0 + } else { val raw6 = (0..5).map { ch -> val chData = channels.find { it.channel == ch } if (chData != null && chData.buffer.isNotEmpty()) @@ -862,6 +884,7 @@ fun PlacementGuideView(appState: AppState) { if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 } } } + } // end of !waitingForStart V2 algorithm } else { // Sphere-fit 계산 (SWEEP 모드에서 사용, 다른 모드에서는 무시됨) val fitResult = com.example.medilightv2android.managers.BladderSphereSeek @@ -1004,30 +1027,40 @@ fun PlacementGuideView(appState: AppState) { Log.d("PlacementGuide", "Scan #$scanCount: ${detected.count { it }}/6, score=$placementScore%") + // Phase 라벨 — V2 활성이면 V2 4-stage phase, 아니면 V1 placementPhase. + // Step 0 (Start 전 baseline) 은 alignment session 에서만 명시. + val isV2 = com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo == + com.example.medilightv2android.managers.AlignmentAlgo.V2 + val clinSess = com.example.medilightv2android.services.ClinicalSessionStore.currentSession + val isClinAlign = clinSess != null && + clinSess.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT + val phaseLabel = when { + isClinAlign && waitingForStart -> "STEP_0_BASELINE" + isV2 -> v2Aligner.phase.name + else -> placementPhase.name + } + logService.logPlacement( channelDetected = detected, direction = directionHint, urineLens = urineLens.toList(), cv = lastCv, lrDev = Double.NaN, // computePlacementGuide 결과의 lrDev는 guideResult에 있음 - phase = placementPhase.name, + phase = phaseLabel, score = placementScore ) // 임상 모드에서는 raw ADC도 adc.csv에 자동 저장. - val clinSess = com.example.medilightv2android.services.ClinicalSessionStore.currentSession if (clinSess != null) { val rawADC = channels.sortedBy { it.channel }.map { it.buffer } com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null) // Alignment session 진행 중이면 measurement.json 용 frame 누적. if (clinSess.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT) { - val isV2 = com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo == - com.example.medilightv2android.managers.AlignmentAlgo.V2 com.example.medilightv2android.services.ClinicalSessionStore.addAlignmentFrame( piezo = channels, imu = lastImuSamples, - // V2 활성 시 4-stage phase 이름 (INITIAL_ACCUM 등), 그 외 V1 placementPhase - alignPhase = if (isV2) v2Aligner.phase.name else placementPhase.name, + // Step 0 baseline / V2 phase / V1 phase 통일된 라벨 사용 + alignPhase = phaseLabel, alignScore = placementScore, alignDetected = (0 until 6).map { channelDetected[it] }, alignUrineLens = (0 until 6).map { channelUrineLen[it] }, @@ -1087,6 +1120,9 @@ fun PlacementGuideView(appState: AppState) { onClick = { waitingForStart = false isContinuousMode = true + // V2 alignment 리셋 — baseline 동안 silent push 가 없었더라도 안전망. + // 사용자 Start 시점부터 fresh accumulation. + v2Aligner.reset() // 전체 상태 리셋 debounceCount = 0 lastHintChangeMs = 0L @@ -1162,7 +1198,19 @@ fun PlacementGuideView(appState: AppState) { // Skip button (태블릿: 15 → 20sp) Spacer(modifier = Modifier.height(8.dp)) - TextButton(onClick = { isContinuousMode = false; appState.placementComplete() }) { + TextButton(onClick = { + isContinuousMode = false + // Clinical alignment session 진입 → abort + Clinical Home 으로 + // (Skip 은 저장 의도 X) + val curSess = com.example.medilightv2android.services.ClinicalSessionStore.currentSession + if (curSess != null && curSess.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT) { + com.example.medilightv2android.services.ClinicalSessionStore.discardAndDeleteFolder() + com.example.medilightv2android.services.ClinicalSessionStore.inClinicalFlow = true + appState.currentScreen = AppScreen.CLINICAL_HOME + } else { + appState.placementComplete() + } + }) { Text( text = "Skip", fontSize = 20.sp,