diff --git a/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt b/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt index 4987570..4c16d72 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt @@ -171,6 +171,13 @@ class AlignGuide4Stage( /** LR_BALANCE 반전 요구 연속 commit 수. 2회 연속 악화여야 방향 반전. * sliding window 잔여 효과 (사용자가 방향 바꿔도 평균은 직전 데이터 섞임) 흡수. */ private val lrReverseConsecutive: Int = 2, + /** + * LR_BALANCE 의 `lost` 감지 hysteresis — N 연속 commit 에서 같은 채널이 + * 빠져있어야 진짜 "회복 필요" 로 인정. 1 cycle noise 깜빡임에 화살표 + * 흔들리는 현상 차단 (2026-06-30 실측 분석 결과 — LR_BALANCE 170 cycle + * 중 CH1=15%, CH3=12% 가 노이즈로 깜빡임 → 화살표 매번 발동). + */ + private val lostStreakRequired: Int = 3, ) { enum class Phase4 { INITIAL_ACCUM, VERTICAL_CLIMB, CH3_STABILIZE, LR_BALANCE } @@ -194,6 +201,7 @@ class AlignGuide4Stage( private var lateralDir: AlignAction? = null private var lateralImbal: Int? = null private var lateralWorseStreak = 0 // LR_BALANCE 연속 악화 카운터 (deadband 초과 증가 시 +1) + private var lostStreak = 0 // LR_BALANCE 채널 lost hysteresis fun reset() { phase = Phase4.INITIAL_ACCUM @@ -203,6 +211,7 @@ class AlignGuide4Stage( lateralDir = null lateralImbal = null lateralWorseStreak = 0 + lostStreak = 0 } fun step(s: AlignSummary): Out { @@ -259,14 +268,24 @@ class AlignGuide4Stage( val setLabel = bestCenterSet.sortedDescending().joinToString("+") { "ch$it" } // dev/debug only val lost = bestCenterSet - curSet if (lost.isNotEmpty()) { - // 잃은 채널 있음 → 회복 우선. ch3 잃었으면 더 위로, 아니면 아래로. - val recoveryDir = if (3 in lost) AlignAction.MOVE_UP else AlignAction.MOVE_DOWN - val arrow = if (recoveryDir == AlignAction.MOVE_UP) "↑" else "↓" - val dirWord = if (recoveryDir == AlignAction.MOVE_UP) "위" else "아래" - val recoveryKey = if (recoveryDir == AlignAction.MOVE_UP) - R.string.align_recover_up_short else R.string.align_recover_down_short - return Out(recoveryDir, "$arrow 위치 살짝 벗어남 — ${dirWord}로 약간", phase, 0, - msgKey = recoveryKey) + // 2026-06-30 hysteresis: N 연속 commit 에서 같은 채널이 lost 여야 진짜 회복 모드. + // 1 cycle 노이즈 깜빡임 (실측 CH1=15%, CH3=12% flap) 에 화살표 흔들리는 현상 차단. + lostStreak++ + if (lostStreak >= lostStreakRequired) { + val recoveryDir = if (3 in lost) AlignAction.MOVE_UP else AlignAction.MOVE_DOWN + val arrow = if (recoveryDir == AlignAction.MOVE_UP) "↑" else "↓" + val dirWord = if (recoveryDir == AlignAction.MOVE_UP) "위" else "아래" + val recoveryKey = if (recoveryDir == AlignAction.MOVE_UP) + R.string.align_recover_up_short else R.string.align_recover_down_short + return Out(recoveryDir, "$arrow 위치 살짝 벗어남 — ${dirWord}로 약간", phase, 0, + msgKey = recoveryKey) + } + // streak 미달 → noise 로 보고 STOP 유지. + return Out(AlignAction.STOP, + "✓ 정렬 완료! 그대로 측정 가능 (noise check $lostStreak/$lostStreakRequired)", + phase, 0, msgKey = R.string.align_complete) + } else { + lostStreak = 0 // full set 복귀 — streak 리셋 } // full set 유지 중 — 좌우 균형 (deadband + consecutive 악화 로 oscillation 차단)