From 1fc1a20bb9aacfec184535d28c9fbef58c8da305 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Mon, 22 Jun 2026 11:26:04 +0900 Subject: [PATCH] =?UTF-8?q?fix(align=20V2):=20LR=5FBALANCE=20=EC=A2=8C?= =?UTF-8?q?=EC=9A=B0=20=ED=99=94=EC=82=B4=ED=91=9C=20oscillation=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=20(deadband=20+=20=EC=97=B0=EC=86=8D=20?= =?UTF-8?q?=EC=95=85=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: ch3 확인 통과 후 LR_BALANCE 단계에서 좌/우 화살표가 빠른 간격으로 번갈아 나타남. 원인: AlignGuide4Stage.LR_BALANCE 의 hill-climbing 반전 조건이 imbal > lateralImbal 만 검사 → 1 sample 라도 늘면 즉시 반전. - 노이즈/호흡 으로 imbal 가 ±1-2 fluctuate → 잘못된 반전 - sliding window 평균 (10-frame) 잔여 효과: 방향 바꿔도 직전 데이터 섞여 imbal 변화 못 따라옴 → 가짜 "악화" 판정 - 사용자 반응 지연 동안 imbal 더 누적 → 또 반전 → ←→←→ 진동 수정 (A + C 조합): 1) A. 반전 deadband (lrReverseDeadband = 3 samples ≈ 6 mm) imbal 가 직전보다 "deadband 초과" 증가해야 악화로 카운트. LAT_TOL(8) 의 약 1/3 — 정렬 완료 조건 위협하지 않으면서 노이즈 흡수. 2) C. 연속 N회 악화 요구 (lrReverseConsecutive = 2) deadband 초과 증가가 2 commit 연속이어야 방향 반전. 단발 노이즈/sliding window 잔여 효과 흡수. 상태 추가: lateralWorseStreak Int (reset 포함). 로직: - 개선 또는 deadband 안 변동 → 같은 방향 유지, streak = 0 - deadband 초과 증가 → streak++ - streak >= 2 → 방향 반전, streak = 0 lateralImbal 은 매 commit 갱신 (기존 동작 유지). Co-Authored-By: Claude Opus 4.7 --- .../managers/AlignmentAdvisorV2.kt | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) 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 29acd14..d05e2cd 100644 --- a/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt +++ b/app/src/main/java/com/example/medilightv2android/managers/AlignmentAdvisorV2.kt @@ -152,6 +152,12 @@ class AlignGuide4Stage( private val latTol: Int = AlignmentConstants.LAT_TOL, private val initialTarget: Int = 10, private val stabilizeTarget: Int = 10, + /** LR_BALANCE 반전 deadband (samples). imbal 가 직전보다 이 값 초과 증가해야 + * '진짜 악화' 로 카운트. 노이즈/호흡 흡수. LAT_TOL(8) 의 약 1/3. */ + private val lrReverseDeadband: Int = 3, + /** LR_BALANCE 반전 요구 연속 commit 수. 2회 연속 악화여야 방향 반전. + * sliding window 잔여 효과 (사용자가 방향 바꿔도 평균은 직전 데이터 섞임) 흡수. */ + private val lrReverseConsecutive: Int = 2, ) { enum class Phase4 { INITIAL_ACCUM, VERTICAL_CLIMB, CH3_STABILIZE, LR_BALANCE } @@ -165,6 +171,7 @@ class AlignGuide4Stage( private var stabilizeCount = 0 private var lateralDir: AlignAction? = null private var lateralImbal: Int? = null + private var lateralWorseStreak = 0 // LR_BALANCE 연속 악화 카운터 (deadband 초과 증가 시 +1) fun reset() { phase = Phase4.INITIAL_ACCUM @@ -173,6 +180,7 @@ class AlignGuide4Stage( stabilizeCount = 0 lateralDir = null lateralImbal = null + lateralWorseStreak = 0 } fun step(s: AlignSummary): Out { @@ -230,7 +238,7 @@ class AlignGuide4Stage( return Out(recoveryDir, "$arrow $lostLabel 잃음 — 위치 회복", phase, 0) } - // full set 유지 중 — 좌우 균형 + // full set 유지 중 — 좌우 균형 (deadband + consecutive 악화 로 oscillation 차단) val u4 = s.u4 val u5 = s.u5 if (u4 != null && u5 != null) { @@ -240,10 +248,24 @@ class AlignGuide4Stage( "■ 정렬 완료 ($setLabel 유지, |Δ|=$imbal)", phase, 0) } if (lateralDir == null) { + // 첫 진입 — u4/u5 비교로 방향 추정 lateralDir = if (u4 < u5) AlignAction.MOVE_RIGHT else AlignAction.MOVE_LEFT - } else if (lateralImbal != null && imbal > lateralImbal!!) { - lateralDir = if (lateralDir == AlignAction.MOVE_LEFT) AlignAction.MOVE_RIGHT - else AlignAction.MOVE_LEFT + lateralWorseStreak = 0 + } else if (lateralImbal != null) { + // 악화 판정: imbal 가 직전보다 deadband 초과로 증가해야 streak++ + val significantlyWorse = imbal > lateralImbal!! + lrReverseDeadband + if (significantlyWorse) { + lateralWorseStreak++ + if (lateralWorseStreak >= lrReverseConsecutive) { + // 연속 N회 진짜 악화 → 방향 반전, streak reset + lateralDir = if (lateralDir == AlignAction.MOVE_LEFT) + AlignAction.MOVE_RIGHT else AlignAction.MOVE_LEFT + lateralWorseStreak = 0 + } + } else { + // 개선 또는 deadband 안의 변동 → 같은 방향 유지, streak reset + lateralWorseStreak = 0 + } } lateralImbal = imbal val arrow = if (lateralDir == AlignAction.MOVE_LEFT) "←" else "→"