258bad9a26
Foreground Service (BleForegroundService.kt): - BLE 연결 시 자동 시작, 해제 시 자동 중단 - PARTIAL_WAKE_LOCK (최대 4시간) - Notification: "VesiScan - BLE 기기 연결 중..." - connectedDevice foregroundServiceType - Doze 모드에서도 BLE 통신 유지 - keepScreenOn도 유지 (추가 안전장치) Detachment 개선: - THR_MEAN/STD 100→30 (완전 탈착만 감지, 거리 약화 무시) - 6채널 모두 수신됐을 때만 체크 (BLE 거리 문제와 구분) - Placement: if(false) 제거 → 활성화 + "센서가 부착되지 않았습니다" - Monitoring: 정상 복귀 시 LED OFF Placement 가이드 모드: - Simple (기본): 3+채널 → 바로 좌우 (기존) - Boundary: 올라갔다 내려오기 패턴 (경계 감지 → 최적점 복귀) - 설정 톱니바퀴에 Simple/Boundary 스위치 BLE 로그: - 역순 표시 (최신이 위) — 스크롤 위치 유지 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
2.9 KiB
Kotlin
94 lines
2.9 KiB
Kotlin
package com.example.medilightv2android.managers
|
|
|
|
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 미만이면 미부착으로 판정.
|
|
*
|
|
* 미부착 시 LED state 4 (DETACH_WARNING) 전송.
|
|
*/
|
|
object DetachmentDetection {
|
|
|
|
const val WINDOW_START = 20
|
|
const val WINDOW_END = 80
|
|
const val BASELINE_WINDOW_START = 80
|
|
const val BASELINE_WINDOW_END = 100
|
|
const val THR_MEAN = 30.0
|
|
const val THR_STD = 30.0
|
|
const val N_CENTER_CH = 4
|
|
|
|
data class DetachmentResult(
|
|
val isDetached: Boolean,
|
|
val meanAbs: Double,
|
|
val stdVal: Double,
|
|
val meanAbsPerCh: DoubleArray,
|
|
val stdPerCh: DoubleArray
|
|
)
|
|
|
|
/**
|
|
* 4채널(CH0~CH3) ADC 신호에서 미부착 여부 판정.
|
|
*
|
|
* @param signals 6채널 ADC 데이터 (List<List<UShort>>)
|
|
* @return DetachmentResult
|
|
*/
|
|
fun detect(signals: List<List<UShort>>): DetachmentResult {
|
|
if (signals.size < N_CENTER_CH) {
|
|
return DetachmentResult(true, 0.0, 0.0, DoubleArray(N_CENTER_CH), DoubleArray(N_CENTER_CH))
|
|
}
|
|
|
|
val nSample = signals[0].size
|
|
val ws = WINDOW_START.coerceAtMost(nSample)
|
|
val we = WINDOW_END.coerceAtMost(nSample)
|
|
val bs = BASELINE_WINDOW_START.coerceAtMost(nSample)
|
|
val be = BASELINE_WINDOW_END.coerceAtMost(nSample)
|
|
|
|
if (we <= ws || be <= bs) {
|
|
return DetachmentResult(true, 0.0, 0.0, DoubleArray(N_CENTER_CH), DoubleArray(N_CENTER_CH))
|
|
}
|
|
|
|
val meanAbsPerCh = DoubleArray(N_CENTER_CH)
|
|
val stdPerCh = DoubleArray(N_CENTER_CH)
|
|
|
|
for (ch in 0 until N_CENTER_CH) {
|
|
val raw = signals[ch]
|
|
if (raw.size < be) {
|
|
meanAbsPerCh[ch] = 0.0
|
|
stdPerCh[ch] = 0.0
|
|
continue
|
|
}
|
|
|
|
// Baseline = median of [bs:be]
|
|
val baselineSlice = (bs until be).map { raw[it].toDouble() }.sorted()
|
|
val baseline = baselineSlice[baselineSlice.size / 2]
|
|
|
|
// y = signal[ws:we] - baseline
|
|
val y = DoubleArray(we - ws) { raw[ws + it].toDouble() - baseline }
|
|
|
|
// mean(|y|)
|
|
meanAbsPerCh[ch] = y.map { abs(it) }.average()
|
|
|
|
// std(y)
|
|
val mean = y.average()
|
|
stdPerCh[ch] = sqrt(y.map { (it - mean) * (it - mean) }.average())
|
|
}
|
|
|
|
val meanAbs = meanAbsPerCh.average()
|
|
val stdVal = stdPerCh.average()
|
|
|
|
// "and" rule: 둘 다 threshold 미만이면 미부착
|
|
val isDetached = meanAbs < THR_MEAN && stdVal < THR_STD
|
|
|
|
return DetachmentResult(
|
|
isDetached = isDetached,
|
|
meanAbs = meanAbs,
|
|
stdVal = stdVal,
|
|
meanAbsPerCh = meanAbsPerCh,
|
|
stdPerCh = stdPerCh
|
|
)
|
|
}
|
|
}
|