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
|
if (isClinicalAlign) com.example.medilightv2android.managers.AlignmentAlgo.V2
|
||||||
else com.example.medilightv2android.managers.AlignmentAlgo.V1
|
else com.example.medilightv2android.managers.AlignmentAlgo.V1
|
||||||
v2Aligner.reset()
|
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
|
waitingForStart = true
|
||||||
placementPhase = PlacementPhase.VERTICAL
|
placementPhase = PlacementPhase.VERTICAL
|
||||||
isLocked = false
|
isLocked = false
|
||||||
@@ -245,7 +248,19 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
@@ -827,6 +842,13 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
} else if (com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo ==
|
} else if (com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo ==
|
||||||
com.example.medilightv2android.managers.AlignmentAlgo.V2) {
|
com.example.medilightv2android.managers.AlignmentAlgo.V2) {
|
||||||
// ═══ V2 alignment (alignment.py 포팅, method_d 기반) ═══
|
// ═══ 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 raw6 = (0..5).map { ch ->
|
||||||
val chData = channels.find { it.channel == ch }
|
val chData = channels.find { it.channel == ch }
|
||||||
if (chData != null && chData.buffer.isNotEmpty())
|
if (chData != null && chData.buffer.isNotEmpty())
|
||||||
@@ -862,6 +884,7 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 }
|
if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // end of !waitingForStart V2 algorithm
|
||||||
} else {
|
} else {
|
||||||
// Sphere-fit 계산 (SWEEP 모드에서 사용, 다른 모드에서는 무시됨)
|
// Sphere-fit 계산 (SWEEP 모드에서 사용, 다른 모드에서는 무시됨)
|
||||||
val fitResult = com.example.medilightv2android.managers.BladderSphereSeek
|
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%")
|
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(
|
logService.logPlacement(
|
||||||
channelDetected = detected,
|
channelDetected = detected,
|
||||||
direction = directionHint,
|
direction = directionHint,
|
||||||
urineLens = urineLens.toList(),
|
urineLens = urineLens.toList(),
|
||||||
cv = lastCv,
|
cv = lastCv,
|
||||||
lrDev = Double.NaN, // computePlacementGuide 결과의 lrDev는 guideResult에 있음
|
lrDev = Double.NaN, // computePlacementGuide 결과의 lrDev는 guideResult에 있음
|
||||||
phase = placementPhase.name,
|
phase = phaseLabel,
|
||||||
score = placementScore
|
score = placementScore
|
||||||
)
|
)
|
||||||
|
|
||||||
// 임상 모드에서는 raw ADC도 adc.csv에 자동 저장.
|
// 임상 모드에서는 raw ADC도 adc.csv에 자동 저장.
|
||||||
val clinSess = com.example.medilightv2android.services.ClinicalSessionStore.currentSession
|
|
||||||
if (clinSess != null) {
|
if (clinSess != null) {
|
||||||
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
||||||
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null)
|
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null)
|
||||||
// Alignment session 진행 중이면 measurement.json 용 frame 누적.
|
// Alignment session 진행 중이면 measurement.json 용 frame 누적.
|
||||||
if (clinSess.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT) {
|
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(
|
com.example.medilightv2android.services.ClinicalSessionStore.addAlignmentFrame(
|
||||||
piezo = channels,
|
piezo = channels,
|
||||||
imu = lastImuSamples,
|
imu = lastImuSamples,
|
||||||
// V2 활성 시 4-stage phase 이름 (INITIAL_ACCUM 등), 그 외 V1 placementPhase
|
// Step 0 baseline / V2 phase / V1 phase 통일된 라벨 사용
|
||||||
alignPhase = if (isV2) v2Aligner.phase.name else placementPhase.name,
|
alignPhase = phaseLabel,
|
||||||
alignScore = placementScore,
|
alignScore = placementScore,
|
||||||
alignDetected = (0 until 6).map { channelDetected[it] },
|
alignDetected = (0 until 6).map { channelDetected[it] },
|
||||||
alignUrineLens = (0 until 6).map { channelUrineLen[it] },
|
alignUrineLens = (0 until 6).map { channelUrineLen[it] },
|
||||||
@@ -1087,6 +1120,9 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
onClick = {
|
onClick = {
|
||||||
waitingForStart = false
|
waitingForStart = false
|
||||||
isContinuousMode = true
|
isContinuousMode = true
|
||||||
|
// V2 alignment 리셋 — baseline 동안 silent push 가 없었더라도 안전망.
|
||||||
|
// 사용자 Start 시점부터 fresh accumulation.
|
||||||
|
v2Aligner.reset()
|
||||||
// 전체 상태 리셋
|
// 전체 상태 리셋
|
||||||
debounceCount = 0
|
debounceCount = 0
|
||||||
lastHintChangeMs = 0L
|
lastHintChangeMs = 0L
|
||||||
@@ -1162,7 +1198,19 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
|
|
||||||
// Skip button (태블릿: 15 → 20sp)
|
// Skip button (태블릿: 15 → 20sp)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
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(
|
||||||
text = "Skip",
|
text = "Skip",
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
|
|||||||
Reference in New Issue
Block a user