fix(alignment): clinical session — V2 phase label + Step 0 baseline + Back/Skip 분기
#1 placements.jsonl phase 라벨 통일 - 기존: placementPhase.name (V1 phase) 만 저장 → V2 활성이어도 V2 phase 안 보임 - 변경: V2 활성이면 v2Aligner.phase.name 사용 (INITIAL_ACCUM 등 4-stage) - measurement.json 의 align_phase 와도 동일 라벨로 통일 #2 Step 0 baseline 데이터 캡쳐 - Clinical alignment session 진입 시 isContinuousMode 자동 ON (waitingForStart 는 true 유지 — 사용자 Start 누를 때까지) - baseline cycles 의 phase = "STEP_0_BASELINE" 로 라벨링 - V2 algorithm push 는 !waitingForStart 일 때만 호출 — silent phase 진행 차단 - Start 누를 때 v2Aligner.reset() 추가 (fresh 시작 보장) #3 Back / Skip 버튼 분기 — clinical alignment session 인 경우 CLINICAL_HOME 으로 - 기존: Back → PIEZO_PERSONALIZATION, Skip → PIEZO_MONITORING (잘못된 화면 이동) - 변경: clinical alignment session 진입 상태면 discardAndDeleteFolder + CLINICAL_HOME (Back/Skip = 저장 의도 X — Complete Alignment 버튼만 저장) - 일반 placement guide 동작은 그대로 유지 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+57
-9
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user