feat(alignment): demo AlignGuide 5-phase 로 확장 — FINAL_CONFIRM 추가
feature/tab-navigation 의 AlignmentAdvisorV2 를 demo 에 포팅 (packages 만 rename). 기존 4-phase (INITIAL_ACCUM → VERTICAL_CLIMB → CH3_STABILIZE → LR_BALANCE) → 5-phase (+ FINAL_CONFIRM). FINAL_CONFIRM phase: - LR_BALANCE 에서 |u4-u5| ≤ 8 도달 시 진입 - buf.clear() 후 5 fresh cycle 누적 - imbal 여전히 ≤ 8 유지 → 최종 STOP - 실패 시 LR_BALANCE 로 복귀 - 목적: sliding window 잔여 효과 제거 후 재검증 추가 항목: - Phase 별 dynamic window (accumKVertical=10, accumKLateral=5, accumKConfirm=5) - lostStreakRequired=3 hysteresis (LR_BALANCE 채널 깜빡임 방지) - msgKey / msgArgs (i18n 지원) Sensor alignment default: - 일반 사용자 (V1): 기존 3-step 유지 (변경 없음) - Clinical alignment session (V2): 5-phase 사용 PlacementGuideView step indicator: 4 → 5. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -165,21 +165,22 @@ class AlignGuide4Stage(
|
|||||||
private val latTol: Int = AlignmentConstants.LAT_TOL,
|
private val latTol: Int = AlignmentConstants.LAT_TOL,
|
||||||
private val initialTarget: Int = 10,
|
private val initialTarget: Int = 10,
|
||||||
private val stabilizeTarget: Int = 10,
|
private val stabilizeTarget: Int = 10,
|
||||||
/** LR_BALANCE 반전 deadband (samples). imbal 가 직전보다 이 값 초과 증가해야
|
/** VERTICAL_CLIMB 에서 CH3_STABILIZE 로 넘어가기 위한 ch3 연속 commit 수. */
|
||||||
* '진짜 악화' 로 카운트. 노이즈/호흡 흡수. LAT_TOL(8) 의 약 1/3. */
|
private val verticalHitRequired: Int = 3,
|
||||||
|
/** LR_BALANCE 반전 deadband (samples). */
|
||||||
private val lrReverseDeadband: Int = 3,
|
private val lrReverseDeadband: Int = 3,
|
||||||
/** LR_BALANCE 반전 요구 연속 commit 수. 2회 연속 악화여야 방향 반전.
|
/** LR_BALANCE 반전 요구 연속 commit 수. */
|
||||||
* sliding window 잔여 효과 (사용자가 방향 바꿔도 평균은 직전 데이터 섞임) 흡수. */
|
|
||||||
private val lrReverseConsecutive: Int = 2,
|
private val lrReverseConsecutive: Int = 2,
|
||||||
/**
|
/**
|
||||||
* LR_BALANCE 의 `lost` 감지 hysteresis — N 연속 commit 에서 같은 채널이
|
* LR_BALANCE 의 `lost` 감지 hysteresis — N 연속 commit 에서 같은 채널이
|
||||||
* 빠져있어야 진짜 "회복 필요" 로 인정. 1 cycle noise 깜빡임에 화살표
|
* 빠져있어야 진짜 "회복 필요" 로 인정. 1 cycle noise 깜빡임에 화살표
|
||||||
* 흔들리는 현상 차단 (2026-06-30 실측 분석 결과 — LR_BALANCE 170 cycle
|
* 흔들리는 현상 차단 (2026-06-30 실측 분석 결과).
|
||||||
* 중 CH1=15%, CH3=12% 가 노이즈로 깜빡임 → 화살표 매번 발동).
|
|
||||||
*/
|
*/
|
||||||
private val lostStreakRequired: Int = 3,
|
private val lostStreakRequired: Int = 3,
|
||||||
|
/** FINAL_CONFIRM 누적 프레임 수 — flowchart 의 \"마지막 측정 5회 반복\". */
|
||||||
|
val finalConfirmTarget: Int = 5,
|
||||||
) {
|
) {
|
||||||
enum class Phase4 { INITIAL_ACCUM, VERTICAL_CLIMB, CH3_STABILIZE, LR_BALANCE }
|
enum class Phase4 { INITIAL_ACCUM, VERTICAL_CLIMB, CH3_STABILIZE, LR_BALANCE, FINAL_CONFIRM }
|
||||||
|
|
||||||
data class Out(
|
data class Out(
|
||||||
val action: AlignAction?,
|
val action: AlignAction?,
|
||||||
@@ -197,20 +198,24 @@ class AlignGuide4Stage(
|
|||||||
var bestCenterSet: Set<Int> = emptySet()
|
var bestCenterSet: Set<Int> = emptySet()
|
||||||
private set
|
private set
|
||||||
private var initialCount = 0
|
private var initialCount = 0
|
||||||
|
private var verticalHitStreak = 0
|
||||||
private var stabilizeCount = 0
|
private var stabilizeCount = 0
|
||||||
private var lateralDir: AlignAction? = null
|
private var lateralDir: AlignAction? = null
|
||||||
private var lateralImbal: Int? = null
|
private var lateralImbal: Int? = null
|
||||||
private var lateralWorseStreak = 0 // LR_BALANCE 연속 악화 카운터 (deadband 초과 증가 시 +1)
|
private var lateralWorseStreak = 0
|
||||||
|
private var finalConfirmCount = 0 // FINAL_CONFIRM 누적 카운터
|
||||||
private var lostStreak = 0 // LR_BALANCE 채널 lost hysteresis
|
private var lostStreak = 0 // LR_BALANCE 채널 lost hysteresis
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
phase = Phase4.INITIAL_ACCUM
|
phase = Phase4.INITIAL_ACCUM
|
||||||
bestCenterSet = emptySet()
|
bestCenterSet = emptySet()
|
||||||
initialCount = 0
|
initialCount = 0
|
||||||
|
verticalHitStreak = 0
|
||||||
stabilizeCount = 0
|
stabilizeCount = 0
|
||||||
lateralDir = null
|
lateralDir = null
|
||||||
lateralImbal = null
|
lateralImbal = null
|
||||||
lateralWorseStreak = 0
|
lateralWorseStreak = 0
|
||||||
|
finalConfirmCount = 0
|
||||||
lostStreak = 0
|
lostStreak = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +226,7 @@ class AlignGuide4Stage(
|
|||||||
if (phase == Phase4.INITIAL_ACCUM) {
|
if (phase == Phase4.INITIAL_ACCUM) {
|
||||||
initialCount++
|
initialCount++
|
||||||
if (initialCount < initialTarget) {
|
if (initialCount < initialTarget) {
|
||||||
return Out(null, "잠시 그대로 두세요 (${initialCount}/${initialTarget})",
|
return Out(null, "측정 준비 중... (${initialCount}/${initialTarget})",
|
||||||
phase, initialCount,
|
phase, initialCount,
|
||||||
msgKey = R.string.align_hold_still,
|
msgKey = R.string.align_hold_still,
|
||||||
msgArgs = listOf(initialCount, initialTarget))
|
msgArgs = listOf(initialCount, initialTarget))
|
||||||
@@ -232,10 +237,21 @@ class AlignGuide4Stage(
|
|||||||
// === Phase 2: VERTICAL_CLIMB ===
|
// === Phase 2: VERTICAL_CLIMB ===
|
||||||
if (phase == Phase4.VERTICAL_CLIMB) {
|
if (phase == Phase4.VERTICAL_CLIMB) {
|
||||||
if (!s.ch3) {
|
if (!s.ch3) {
|
||||||
return Out(AlignAction.MOVE_UP, "↑ 위로 조금씩 올리세요",
|
verticalHitStreak = 0 // 연속 카운터 리셋 — false detection 거부
|
||||||
|
return Out(AlignAction.MOVE_UP, "↑ 위로 조금씩 올리세요 (ch3 미검출)",
|
||||||
phase, 0,
|
phase, 0,
|
||||||
msgKey = R.string.align_move_up)
|
msgKey = R.string.align_move_up)
|
||||||
}
|
}
|
||||||
|
verticalHitStreak++
|
||||||
|
if (verticalHitStreak < verticalHitRequired) {
|
||||||
|
// ch3 일시 검출 — verticalHitRequired 회 연속 확인 전까지 사용자 정지.
|
||||||
|
// (현재 위치가 진짜 ch3 인지 평균이 만든 false echo 인지 확인 중)
|
||||||
|
return Out(AlignAction.STOP,
|
||||||
|
"✓ ch3 일시 검출 — 확인 중 (${verticalHitStreak}/${verticalHitRequired})",
|
||||||
|
phase, verticalHitStreak,
|
||||||
|
msgKey = R.string.align_hold_position,
|
||||||
|
msgArgs = listOf(verticalHitStreak, verticalHitRequired))
|
||||||
|
}
|
||||||
phase = Phase4.CH3_STABILIZE
|
phase = Phase4.CH3_STABILIZE
|
||||||
stabilizeCount = 0
|
stabilizeCount = 0
|
||||||
bestCenterSet = curSet
|
bestCenterSet = curSet
|
||||||
@@ -249,13 +265,13 @@ class AlignGuide4Stage(
|
|||||||
// ch3 잃었으면 → VERTICAL_CLIMB 회귀
|
// ch3 잃었으면 → VERTICAL_CLIMB 회귀
|
||||||
phase = Phase4.VERTICAL_CLIMB
|
phase = Phase4.VERTICAL_CLIMB
|
||||||
stabilizeCount = 0
|
stabilizeCount = 0
|
||||||
return Out(AlignAction.MOVE_UP, "↑ 살짝 놓침 — 다시 위로",
|
return Out(AlignAction.MOVE_UP, "↑ ch3 다시 잃음 — 위로",
|
||||||
phase, 0,
|
phase, 0,
|
||||||
msgKey = R.string.align_recover_up)
|
msgKey = R.string.align_recover_up)
|
||||||
}
|
}
|
||||||
if (stabilizeCount < stabilizeTarget) {
|
if (stabilizeCount < stabilizeTarget) {
|
||||||
return Out(AlignAction.STOP,
|
return Out(AlignAction.STOP,
|
||||||
"✓ 좋아요! 그 위치 유지 (${stabilizeCount}/${stabilizeTarget})",
|
"✓ ch3 검출! 위치 유지 (${stabilizeCount}/${stabilizeTarget})",
|
||||||
phase, stabilizeCount,
|
phase, stabilizeCount,
|
||||||
msgKey = R.string.align_hold_position,
|
msgKey = R.string.align_hold_position,
|
||||||
msgArgs = listOf(stabilizeCount, stabilizeTarget))
|
msgArgs = listOf(stabilizeCount, stabilizeTarget))
|
||||||
@@ -264,8 +280,9 @@ class AlignGuide4Stage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === Phase 4: LR_BALANCE ===
|
// === Phase 4: LR_BALANCE ===
|
||||||
|
if (phase == Phase4.LR_BALANCE) {
|
||||||
if (curSet.size > bestCenterSet.size) bestCenterSet = curSet
|
if (curSet.size > bestCenterSet.size) bestCenterSet = curSet
|
||||||
val setLabel = bestCenterSet.sortedDescending().joinToString("+") { "ch$it" } // dev/debug only
|
val setLabel = bestCenterSet.sortedDescending().joinToString("+") { "ch$it" }
|
||||||
val lost = bestCenterSet - curSet
|
val lost = bestCenterSet - curSet
|
||||||
if (lost.isNotEmpty()) {
|
if (lost.isNotEmpty()) {
|
||||||
// 2026-06-30 hysteresis: N 연속 commit 에서 같은 채널이 lost 여야 진짜 회복 모드.
|
// 2026-06-30 hysteresis: N 연속 commit 에서 같은 채널이 lost 여야 진짜 회복 모드.
|
||||||
@@ -273,17 +290,17 @@ class AlignGuide4Stage(
|
|||||||
lostStreak++
|
lostStreak++
|
||||||
if (lostStreak >= lostStreakRequired) {
|
if (lostStreak >= lostStreakRequired) {
|
||||||
val recoveryDir = if (3 in lost) AlignAction.MOVE_UP else AlignAction.MOVE_DOWN
|
val recoveryDir = if (3 in lost) AlignAction.MOVE_UP else AlignAction.MOVE_DOWN
|
||||||
|
val lostLabel = lost.sortedDescending().joinToString(",") { "ch$it" }
|
||||||
val arrow = if (recoveryDir == AlignAction.MOVE_UP) "↑" else "↓"
|
val arrow = if (recoveryDir == AlignAction.MOVE_UP) "↑" else "↓"
|
||||||
val dirWord = if (recoveryDir == AlignAction.MOVE_UP) "위" else "아래"
|
|
||||||
val recoveryKey = if (recoveryDir == AlignAction.MOVE_UP)
|
val recoveryKey = if (recoveryDir == AlignAction.MOVE_UP)
|
||||||
R.string.align_recover_up_short else R.string.align_recover_down_short
|
R.string.align_recover_up_short else R.string.align_recover_down_short
|
||||||
return Out(recoveryDir, "$arrow 위치 살짝 벗어남 — ${dirWord}로 약간", phase, 0,
|
return Out(recoveryDir, "$arrow $lostLabel 잃음 — 위치 회복", phase, 0,
|
||||||
msgKey = recoveryKey)
|
msgKey = recoveryKey)
|
||||||
}
|
}
|
||||||
// streak 미달 → noise 로 보고 STOP 유지.
|
// streak 미달 → noise 로 보고 STOP 유지.
|
||||||
return Out(AlignAction.STOP,
|
return Out(AlignAction.STOP,
|
||||||
"✓ 정렬 완료! 그대로 측정 가능 (noise check $lostStreak/$lostStreakRequired)",
|
"✓ 그대로 측정 가능 (noise check $lostStreak/$lostStreakRequired)", phase, 0,
|
||||||
phase, 0, msgKey = R.string.align_complete)
|
msgKey = R.string.align_complete)
|
||||||
} else {
|
} else {
|
||||||
lostStreak = 0 // full set 복귀 — streak 리셋
|
lostStreak = 0 // full set 복귀 — streak 리셋
|
||||||
}
|
}
|
||||||
@@ -294,42 +311,100 @@ class AlignGuide4Stage(
|
|||||||
if (u4 != null && u5 != null) {
|
if (u4 != null && u5 != null) {
|
||||||
val imbal = kotlin.math.abs(u4 - u5)
|
val imbal = kotlin.math.abs(u4 - u5)
|
||||||
if (imbal <= latTol) {
|
if (imbal <= latTol) {
|
||||||
|
// 균형 도달 → FINAL_CONFIRM 으로 전이. RollingAligner 가 buf clear → 5 fresh 프레임 누적.
|
||||||
|
phase = Phase4.FINAL_CONFIRM
|
||||||
|
finalConfirmCount = 0
|
||||||
return Out(AlignAction.STOP,
|
return Out(AlignAction.STOP,
|
||||||
"✓ 정렬 완료! 그대로 측정 가능", phase, 0,
|
"✓ 균형 도달 — 최종 확인 중 (0/${finalConfirmTarget})", phase, 0,
|
||||||
msgKey = R.string.align_complete)
|
msgKey = R.string.align_hold_position,
|
||||||
|
msgArgs = listOf(0, finalConfirmTarget))
|
||||||
}
|
}
|
||||||
if (lateralDir == null) {
|
if (lateralDir == null) {
|
||||||
// 첫 진입 — u4/u5 비교로 방향 추정
|
|
||||||
lateralDir = if (u4 < u5) AlignAction.MOVE_RIGHT else AlignAction.MOVE_LEFT
|
lateralDir = if (u4 < u5) AlignAction.MOVE_RIGHT else AlignAction.MOVE_LEFT
|
||||||
lateralWorseStreak = 0
|
lateralWorseStreak = 0
|
||||||
} else if (lateralImbal != null) {
|
} else if (lateralImbal != null) {
|
||||||
// 악화 판정: imbal 가 직전보다 deadband 초과로 증가해야 streak++
|
|
||||||
val significantlyWorse = imbal > lateralImbal!! + lrReverseDeadband
|
val significantlyWorse = imbal > lateralImbal!! + lrReverseDeadband
|
||||||
if (significantlyWorse) {
|
if (significantlyWorse) {
|
||||||
lateralWorseStreak++
|
lateralWorseStreak++
|
||||||
if (lateralWorseStreak >= lrReverseConsecutive) {
|
if (lateralWorseStreak >= lrReverseConsecutive) {
|
||||||
// 연속 N회 진짜 악화 → 방향 반전, streak reset
|
|
||||||
lateralDir = if (lateralDir == AlignAction.MOVE_LEFT)
|
lateralDir = if (lateralDir == AlignAction.MOVE_LEFT)
|
||||||
AlignAction.MOVE_RIGHT else AlignAction.MOVE_LEFT
|
AlignAction.MOVE_RIGHT else AlignAction.MOVE_LEFT
|
||||||
lateralWorseStreak = 0
|
lateralWorseStreak = 0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 개선 또는 deadband 안의 변동 → 같은 방향 유지, streak reset
|
|
||||||
lateralWorseStreak = 0
|
lateralWorseStreak = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lateralImbal = imbal
|
lateralImbal = imbal
|
||||||
val arrow = if (lateralDir == AlignAction.MOVE_LEFT) "←" else "→"
|
val arrow = if (lateralDir == AlignAction.MOVE_LEFT) "←" else "→"
|
||||||
val sideWord = if (lateralDir == AlignAction.MOVE_LEFT) "왼쪽" else "오른쪽"
|
|
||||||
val sideKey = if (lateralDir == AlignAction.MOVE_LEFT)
|
val sideKey = if (lateralDir == AlignAction.MOVE_LEFT)
|
||||||
R.string.align_left else R.string.align_right
|
R.string.align_left else R.string.align_right
|
||||||
return Out(lateralDir!!, "$arrow ${sideWord}으로 살짝", phase, 0,
|
return Out(lateralDir!!, "$arrow $setLabel 유지 + 좌우 균형 (|Δ|=$imbal)",
|
||||||
|
phase, 0,
|
||||||
msgKey = sideKey)
|
msgKey = sideKey)
|
||||||
}
|
}
|
||||||
// u4/u5 미검출 → probe
|
return Out(AlignAction.PROBE_LR, "↔ $setLabel 유지 + 좌우 천천히 (lateral 미검출)",
|
||||||
return Out(AlignAction.PROBE_LR, "↔ 좌우로 천천히 움직여 보세요", phase, 0,
|
phase, 0,
|
||||||
msgKey = R.string.align_probe_lr)
|
msgKey = R.string.align_probe_lr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Phase 5: FINAL_CONFIRM ===
|
||||||
|
// RollingAligner 가 진입 시 buf 를 clear → 5 fresh 프레임 누적 후 imbal 재평가.
|
||||||
|
if (phase == Phase4.FINAL_CONFIRM) {
|
||||||
|
// center set 유지 확인 — 잃었으면 LR_BALANCE 회귀.
|
||||||
|
val lost = bestCenterSet - curSet
|
||||||
|
if (lost.isNotEmpty()) {
|
||||||
|
val recoveryDir = if (3 in lost) AlignAction.MOVE_UP else AlignAction.MOVE_DOWN
|
||||||
|
val arrow = if (recoveryDir == AlignAction.MOVE_UP) "↑" else "↓"
|
||||||
|
phase = Phase4.LR_BALANCE
|
||||||
|
finalConfirmCount = 0
|
||||||
|
val recoveryKey = if (recoveryDir == AlignAction.MOVE_UP)
|
||||||
|
R.string.align_recover_up_short else R.string.align_recover_down_short
|
||||||
|
return Out(recoveryDir, "$arrow 채널 잃음 — 재정렬", phase, 0,
|
||||||
|
msgKey = recoveryKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalConfirmCount++
|
||||||
|
|
||||||
|
// 누적 중 — 5 프레임 모일 때까지 정지 안내
|
||||||
|
if (finalConfirmCount < finalConfirmTarget) {
|
||||||
|
return Out(AlignAction.STOP,
|
||||||
|
"✓ 최종 확인 중 (${finalConfirmCount}/${finalConfirmTarget})",
|
||||||
|
phase, finalConfirmCount,
|
||||||
|
msgKey = R.string.align_hold_position,
|
||||||
|
msgArgs = listOf(finalConfirmCount, finalConfirmTarget))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 프레임 도달 — imbal 재평가
|
||||||
|
val u4 = s.u4; val u5 = s.u5
|
||||||
|
if (u4 == null || u5 == null) {
|
||||||
|
phase = Phase4.LR_BALANCE
|
||||||
|
finalConfirmCount = 0
|
||||||
|
return Out(AlignAction.PROBE_LR, "lateral 미검출 — 좌우 재확인", phase, 0,
|
||||||
|
msgKey = R.string.align_probe_lr)
|
||||||
|
}
|
||||||
|
val imbal = kotlin.math.abs(u4 - u5)
|
||||||
|
val setLabel = bestCenterSet.sortedDescending().joinToString("+") { "ch$it" }
|
||||||
|
return if (imbal <= latTol) {
|
||||||
|
// 최종 확인 통과 → 진짜 STOP
|
||||||
|
Out(AlignAction.STOP,
|
||||||
|
"■ 정렬 완료 ($setLabel, 확인 OK, |Δ|=$imbal)", phase, finalConfirmTarget,
|
||||||
|
msgKey = R.string.align_complete)
|
||||||
|
} else {
|
||||||
|
// 재평가 실패 → LR_BALANCE 회귀하여 추가 조정
|
||||||
|
phase = Phase4.LR_BALANCE
|
||||||
|
finalConfirmCount = 0
|
||||||
|
val arrow = if (u4 < u5) "→" else "←"
|
||||||
|
val sideKey = if (u4 < u5) R.string.align_right else R.string.align_left
|
||||||
|
Out(if (u4 < u5) AlignAction.MOVE_RIGHT else AlignAction.MOVE_LEFT,
|
||||||
|
"$arrow 재조정 필요 — 좌우 균형 (|Δ|=$imbal)", phase, 0,
|
||||||
|
msgKey = sideKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 도달 불가 (defensive)
|
||||||
|
return Out(AlignAction.STOP, "unknown phase: $phase", phase, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -343,36 +418,55 @@ class AlignGuide4Stage(
|
|||||||
*/
|
*/
|
||||||
class RollingAligner(
|
class RollingAligner(
|
||||||
private val latTol: Int = AlignmentConstants.LAT_TOL,
|
private val latTol: Int = AlignmentConstants.LAT_TOL,
|
||||||
private val accumK: Int = 10,
|
/** INITIAL_ACCUM / VERTICAL_CLIMB / CH3_STABILIZE 단계 window 크기. flowchart 의 \"10회 측정 평균\". */
|
||||||
|
private val accumKVertical: Int = 10,
|
||||||
|
/** LR_BALANCE 단계 window 크기. flowchart 의 \"5회 측정 평균\" (빠른 반응). */
|
||||||
|
private val accumKLateral: Int = 5,
|
||||||
|
/** FINAL_CONFIRM 단계 window 크기. flowchart 의 \"마지막 5회 반복 측정\". */
|
||||||
|
private val accumKConfirm: Int = 5,
|
||||||
private val methodDParams: MethodDParams = MethodDParams.DEFAULT,
|
private val methodDParams: MethodDParams = MethodDParams.DEFAULT,
|
||||||
) {
|
) {
|
||||||
private val buf = ArrayDeque<List<DoubleArray>>()
|
private val buf = ArrayDeque<List<DoubleArray>>()
|
||||||
private val guide4 = AlignGuide4Stage(latTol = latTol,
|
private val guide4 = AlignGuide4Stage(latTol = latTol,
|
||||||
initialTarget = accumK, stabilizeTarget = accumK)
|
initialTarget = accumKVertical, stabilizeTarget = accumKVertical,
|
||||||
/** 마지막 commit 결과. 외부 query 용. */
|
finalConfirmTarget = accumKConfirm)
|
||||||
|
private var lastSeenPhase: AlignGuide4Stage.Phase4? = null
|
||||||
|
/** 마지막 commit 결과. */
|
||||||
var lastCommit: AdvisorState? = null
|
var lastCommit: AdvisorState? = null
|
||||||
private set
|
private set
|
||||||
/** 현재 4-stage phase — UI step indicator 용. */
|
|
||||||
val phase: AlignGuide4Stage.Phase4 get() = guide4.phase
|
val phase: AlignGuide4Stage.Phase4 get() = guide4.phase
|
||||||
/** 지금까지 검출된 최대 center channel set — Phase 4 의 lock 대상. */
|
|
||||||
val bestCenterSet: Set<Int> get() = guide4.bestCenterSet
|
val bestCenterSet: Set<Int> get() = guide4.bestCenterSet
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
buf.clear()
|
|
||||||
guide4.reset()
|
|
||||||
lastAvg = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** UI 그래프에서 N-cycle 평균 신호를 보여주기 위해 마지막 avg 노출. */
|
/** UI 그래프에서 N-cycle 평균 신호를 보여주기 위해 마지막 avg 노출. */
|
||||||
var lastAvg: List<DoubleArray>? = null
|
var lastAvg: List<DoubleArray>? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
buf.clear()
|
||||||
|
guide4.reset()
|
||||||
|
lastSeenPhase = null
|
||||||
|
lastAvg = null
|
||||||
|
}
|
||||||
|
|
||||||
/** 단일 채널 길이 100 가정. raw6.size == 6 이어야 함. */
|
/** 단일 채널 길이 100 가정. raw6.size == 6 이어야 함. */
|
||||||
fun push(raw6: List<DoubleArray>): AdvisorState {
|
fun push(raw6: List<DoubleArray>): AdvisorState {
|
||||||
// ring buffer — accumK 사이즈 sliding window. buf 가 덜 찼어도 항상 step 호출
|
// Phase 전이 감지 — FINAL_CONFIRM 진입 시 buf clear 로 fresh 5 프레임 누적 보장.
|
||||||
// (점진 평균 + 4-stage 가 INITIAL_ACCUM 으로 처리).
|
// (flowchart \"마지막 측정 및 확인 — 5회 반복 측정\")
|
||||||
|
val curPhase = guide4.phase
|
||||||
|
if (lastSeenPhase != null && lastSeenPhase != AlignGuide4Stage.Phase4.FINAL_CONFIRM
|
||||||
|
&& curPhase == AlignGuide4Stage.Phase4.FINAL_CONFIRM) {
|
||||||
|
buf.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 별 동적 window 크기.
|
||||||
|
val windowSize = when (curPhase) {
|
||||||
|
AlignGuide4Stage.Phase4.LR_BALANCE -> accumKLateral
|
||||||
|
AlignGuide4Stage.Phase4.FINAL_CONFIRM -> accumKConfirm
|
||||||
|
else -> accumKVertical
|
||||||
|
}
|
||||||
|
|
||||||
buf.addLast(raw6)
|
buf.addLast(raw6)
|
||||||
while (buf.size > accumK) buf.removeFirst()
|
while (buf.size > windowSize) buf.removeFirst()
|
||||||
|
|
||||||
val nCh = raw6.size
|
val nCh = raw6.size
|
||||||
val avg = List(nCh) { ch ->
|
val avg = List(nCh) { ch ->
|
||||||
@@ -387,13 +481,23 @@ class RollingAligner(
|
|||||||
val dets = MethodDRunner.detectMultichannel(avg, methodDParams)
|
val dets = MethodDRunner.detectMultichannel(avg, methodDParams)
|
||||||
val summary = summarize(dets)
|
val summary = summarize(dets)
|
||||||
val out = guide4.step(summary)
|
val out = guide4.step(summary)
|
||||||
val state = if (out.phase == AlignGuide4Stage.Phase4.INITIAL_ACCUM) "accum" else "commit"
|
lastSeenPhase = guide4.phase
|
||||||
|
|
||||||
|
// accum state — INITIAL_ACCUM 의 첫 N 프레임 + FINAL_CONFIRM 누적 중.
|
||||||
|
val isAccumulating = out.phase == AlignGuide4Stage.Phase4.INITIAL_ACCUM ||
|
||||||
|
(out.phase == AlignGuide4Stage.Phase4.FINAL_CONFIRM &&
|
||||||
|
out.progress in 1 until accumKConfirm)
|
||||||
|
val state = if (isAccumulating) "accum" else "commit"
|
||||||
|
val effectiveTarget = when (out.phase) {
|
||||||
|
AlignGuide4Stage.Phase4.FINAL_CONFIRM -> accumKConfirm
|
||||||
|
else -> accumKVertical
|
||||||
|
}
|
||||||
val st = AdvisorState(
|
val st = AdvisorState(
|
||||||
state = state, action = out.action, msg = out.msg,
|
state = state, action = out.action, msg = out.msg,
|
||||||
msgKey = out.msgKey, msgArgs = out.msgArgs,
|
msgKey = out.msgKey, msgArgs = out.msgArgs,
|
||||||
dets = dets, summary = summary,
|
dets = dets, summary = summary,
|
||||||
accumProgress = if (state == "accum") out.progress else accumK,
|
accumProgress = if (state == "accum") out.progress else effectiveTarget,
|
||||||
accumTarget = accumK,
|
accumTarget = effectiveTarget,
|
||||||
)
|
)
|
||||||
lastCommit = st
|
lastCommit = st
|
||||||
return st
|
return st
|
||||||
|
|||||||
+4
-2
@@ -573,7 +573,8 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
) {
|
) {
|
||||||
// V2 활성 시 4-stage step indicator, 아니면 V1 의 3-stage.
|
// V2 활성 시 5-stage step indicator, 아니면 V1 의 3-stage.
|
||||||
|
// 2026-07-01: FINAL_CONFIRM phase 추가 (feature/tab-navigation 동기화).
|
||||||
val isV2Active = com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo ==
|
val isV2Active = com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo ==
|
||||||
com.example.medilightv2android.managers.AlignmentAlgo.V2
|
com.example.medilightv2android.managers.AlignmentAlgo.V2
|
||||||
val (currentStepIdx, totalSteps) = if (isV2Active) {
|
val (currentStepIdx, totalSteps) = if (isV2Active) {
|
||||||
@@ -582,8 +583,9 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.VERTICAL_CLIMB -> 1
|
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.VERTICAL_CLIMB -> 1
|
||||||
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.CH3_STABILIZE -> 2
|
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.CH3_STABILIZE -> 2
|
||||||
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.LR_BALANCE -> 3
|
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.LR_BALANCE -> 3
|
||||||
|
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.FINAL_CONFIRM -> 4
|
||||||
}
|
}
|
||||||
idx to 4
|
idx to 5
|
||||||
} else {
|
} else {
|
||||||
val idx = when (placementPhase) {
|
val idx = when (placementPhase) {
|
||||||
PlacementPhase.VERTICAL -> 0
|
PlacementPhase.VERTICAL -> 0
|
||||||
|
|||||||
Reference in New Issue
Block a user