diff --git a/app/src/main/java/com/medithings/vesiscan/managers/DetachmentDetection.kt b/app/src/main/java/com/medithings/vesiscan/managers/DetachmentDetection.kt index 37942fa..f816c45 100644 --- a/app/src/main/java/com/medithings/vesiscan/managers/DetachmentDetection.kt +++ b/app/src/main/java/com/medithings/vesiscan/managers/DetachmentDetection.kt @@ -4,12 +4,44 @@ import kotlin.math.abs import kotlin.math.sqrt /** - * 센서 미부착 감지 — detachment_detection.py 1:1 포팅 + * 센서 미부착 감지. * - * ADC 신호의 [20:80] 구간에서 baseline([80:100] median)을 빼고, - * mean(|y|)과 std(y)가 모두 threshold 미만이면 미부착으로 판정. + * 원본: `vesiscan_test/library/alignment/detachment_detection.py` * - * 미부착 시 LED state 4 (DETACH_WARNING) 전송. + * ADC 신호의 [20:80] 구간에서 baseline([80:100] median)을 빼고, CH0~CH3 의 + * mean(|y|)·std(y) 를 평균 내 임계와 비교한다. 미부착 시 LED state 4 + * (DETACH_WARNING) 전송. + * + * ## ⚠ 원본과 값이 다르다 — 되돌리지 말 것 + * + * | | Python | 여기 | + * |---|---|---| + * | 임계 | 100 | **30** | + * | 규칙 | AND | **OR** | + * + * 최초 포팅(`ea19b74`, 2026-04-24)은 100·AND 로 원본과 같았다. 두 번 바뀌었고 + * **둘 다 실기기 증상을 보고 내린 결정**이다. + * + * **100 → 30** (`258bad9`, 2026-05-04) — "완전 탈착만 감지, 거리 약화 무시". + * 원본의 100 은 **팬텀** 실측 분포에서 나온 값이고, 그 두 조건은 *미부착* 과 + * *incorrect(잘못 붙음)* 이다. 실기기에는 조건이 하나 더 있다 — **붙었는데 신호가 + * 약한 경우**(밀착 부족·체형·젤 상태). 그건 미부착이 아닌데 100 을 쓰면 그것까지 + * 잡아서, 멀쩡히 측정 중인 사용자에게 경고가 계속 뜬다. + * + * **AND → OR** (`b2c1ba2`, 2026-08-11) — 사용자 보고. 센서가 아예 분리됐는데 공기 중 + * EMI/RF 로 std 가 30 을 넘어, 둘 다 30 미만을 요구하는 AND 에서는 판정이 몇 초 늦었다. + * + * ## parity 규칙이 여기엔 걸리지 않는다 + * `SPEC.md` 의 Python↔Kotlin 대조 계약은 §5(파이프라인)·§8(정렬) 같은 등재 항목에 + * 적용된다. detachment 는 SPEC 에 없고, **Python 쪽에서도 이 모듈을 부르는 코드가 + * 하나도 없다**(파이프라인 밖 독립 유틸). 검증도 팬텀에서만 했다. + * + * ## 남은 숙제 + * 두 변경이 서로를 가린다. OR 로 바꾼 근거("std 가 30 이상인 경우가 흔하다")는 + * **임계가 30 이라서** 생긴 문제다 — 100 이었다면 std<100 은 쉽게 걸려 AND 로도 + * 즉시 판정됐을 것이다. 30+OR / 30+AND / 100+AND 중 실기기에서 무엇이 나은지는 + * 비교된 적이 없다. 임상 화면의 채널 접촉 칸이 mean·std 실측값을 띄우므로 + * ([DetachWatcher]), 실제로 뜬 순간과 약하게 붙은 순간의 숫자를 모아 정할 것. */ object DetachmentDetection { @@ -17,8 +49,14 @@ object DetachmentDetection { const val WINDOW_END = 80 const val BASELINE_WINDOW_START = 80 const val BASELINE_WINDOW_END = 100 + + /** Python `DEFAULT_THR_MEAN` = 100. 위 "원본과 값이 다르다" 참조. */ const val THR_MEAN = 30.0 + + /** Python `DEFAULT_THR_STD` = 100. 위 "원본과 값이 다르다" 참조. */ const val THR_STD = 30.0 + + /** Python `N_CENTER_CH`. CH0~CH3 만 본다 — lateral 은 임계 근거가 없다. */ const val N_CENTER_CH = 4 data class DetachmentResult(