feat: Foreground Service + Detachment 개선 + Placement 가이드 모드
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>
This commit is contained in:
@@ -46,6 +46,7 @@ class MainActivity : ComponentActivity() {
|
||||
NotificationService.getInstance(this)
|
||||
|
||||
enableEdgeToEdge()
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setContent {
|
||||
Medilightv2androidTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
|
||||
@@ -245,6 +245,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
connectionTimer?.let { handler.removeCallbacks(it) }
|
||||
connectionTimer = null
|
||||
bluetoothGatt?.disconnect()
|
||||
com.example.medilightv2android.services.BleForegroundService.stop(context)
|
||||
}
|
||||
|
||||
fun disconnectAndUnbond() {
|
||||
@@ -252,6 +253,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
cancelAutoReconnect()
|
||||
stopWatchdog()
|
||||
stopBatteryPolling()
|
||||
com.example.medilightv2android.services.BleForegroundService.stop(context)
|
||||
connectionTimer?.let { handler.removeCallbacks(it) }
|
||||
connectionTimer = null
|
||||
|
||||
@@ -274,7 +276,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
connectedDeviceName.value = ""
|
||||
batteryLevel.value = 0
|
||||
lastConnectedAddress = null
|
||||
|
||||
|
||||
// Android 측 본딩 정보도 삭제
|
||||
device?.let { dev ->
|
||||
try {
|
||||
@@ -790,6 +792,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
if (txCharacteristic != null && rxCharacteristic != null) {
|
||||
isServiceReady.value = true
|
||||
com.example.medilightv2android.services.AdcCsvLogger.newSession()
|
||||
com.example.medilightv2android.services.BleForegroundService.start(context)
|
||||
startBatteryPolling()
|
||||
startWatchdog()
|
||||
sendDeviceInfoQuery()
|
||||
|
||||
@@ -17,8 +17,8 @@ object DetachmentDetection {
|
||||
const val WINDOW_END = 80
|
||||
const val BASELINE_WINDOW_START = 80
|
||||
const val BASELINE_WINDOW_END = 100
|
||||
const val THR_MEAN = 100.0
|
||||
const val THR_STD = 100.0
|
||||
const val THR_MEAN = 30.0
|
||||
const val THR_STD = 30.0
|
||||
const val N_CENTER_CH = 4
|
||||
|
||||
data class DetachmentResult(
|
||||
|
||||
@@ -13,10 +13,12 @@ package com.example.medilightv2android.managers
|
||||
* 각 상수의 의미와 영향 범위를 아래 주석 참고.
|
||||
*/
|
||||
enum class DetectionMethod { METHOD_A, METHOD_B, METHOD_C }
|
||||
enum class PlacementGuideMode { SIMPLE, BOUNDARY }
|
||||
|
||||
object GreenZoneConstants {
|
||||
|
||||
var detectionMethod: DetectionMethod = DetectionMethod.METHOD_C
|
||||
var placementGuideMode: PlacementGuideMode = PlacementGuideMode.SIMPLE
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 신호 범위
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.example.medilightv2android.services
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.example.medilightv2android.MainActivity
|
||||
import com.example.medilightv2android.R
|
||||
|
||||
/**
|
||||
* BLE 연결 유지를 위한 Foreground Service.
|
||||
*
|
||||
* 화면이 꺼져도 Android Doze 모드에서 앱을 유지하여
|
||||
* BLE 통신(TX/RX), watchdog, Auto 측정이 중단되지 않도록 한다.
|
||||
*/
|
||||
class BleForegroundService : Service() {
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "vesiscan_ble_channel"
|
||||
const val NOTIFICATION_ID = 1001
|
||||
const val ACTION_START = "com.example.medilightv2android.BLE_START"
|
||||
const val ACTION_STOP = "com.example.medilightv2android.BLE_STOP"
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, BleForegroundService::class.java).apply {
|
||||
action = ACTION_START
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val intent = Intent(context, BleForegroundService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
releaseWakeLock()
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
else -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIFICATION_ID, buildNotification(),
|
||||
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, buildNotification())
|
||||
}
|
||||
acquireWakeLock()
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseWakeLock()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"VesiScan BLE Connection",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "BLE 기기 연결 유지"
|
||||
setShowBadge(false)
|
||||
}
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
nm.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0,
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("VesiScan")
|
||||
.setContentText("BLE 기기 연결 중...")
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun acquireWakeLock() {
|
||||
if (wakeLock == null) {
|
||||
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
wakeLock = pm.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"VesiScan::BleWakeLock"
|
||||
).apply {
|
||||
acquire(4 * 60 * 60 * 1000L) // 최대 4시간
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseWakeLock() {
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) it.release()
|
||||
}
|
||||
wakeLock = null
|
||||
}
|
||||
}
|
||||
+17
-10
@@ -123,13 +123,19 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
lastMultiChannelResult.clear()
|
||||
lastMultiChannelResult.addAll(channels)
|
||||
|
||||
// Detachment 체크
|
||||
val sortedBuffers = channels.sortedBy { it.channel }.map { it.buffer }
|
||||
val detachResult = com.example.medilightv2android.managers.DetachmentDetection.detect(sortedBuffers)
|
||||
if (detachResult.isDetached) {
|
||||
Log.w("PiezoMonitor", "DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||
bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||
handler.post { bleManager.sendLedMode(4) }
|
||||
// Detachment 체크 (6채널 모두 수신됐을 때만 — BLE 거리 문제와 구분)
|
||||
val fullChannels = channels.filter { it.buffer.size >= 20 }
|
||||
if (fullChannels.size >= 6) {
|
||||
val sortedBuffers = channels.sortedBy { it.channel }.map { it.buffer }
|
||||
val detachResult = com.example.medilightv2android.managers.DetachmentDetection.detect(sortedBuffers)
|
||||
if (detachResult.isDetached) {
|
||||
Log.w("PiezoMonitor", "DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||
bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||
handler.post { bleManager.sendLedMode(4) }
|
||||
} else {
|
||||
// 정상 복귀 → LED OFF
|
||||
handler.post { bleManager.sendLedMode(0) }
|
||||
}
|
||||
}
|
||||
|
||||
var rawVolumeMl: Double? = null
|
||||
@@ -1375,7 +1381,7 @@ private fun BleDebugPanel(bleManager: BleManager) {
|
||||
}
|
||||
}
|
||||
|
||||
val entries = remember(refreshTick) { logger.getRecentEntries(50) }
|
||||
val entries = remember(refreshTick) { logger.getRecentEntries(50).reversed() }
|
||||
val stats = remember(refreshTick) { logger.getStats() }
|
||||
|
||||
Card(
|
||||
@@ -1439,12 +1445,13 @@ private fun BleDebugPanel(bleManager: BleManager) {
|
||||
if (timeout > 0) Text("TOUT:$timeout", fontSize = 10.sp, color = Color(0xFFFF8A65))
|
||||
}
|
||||
|
||||
// Log entries
|
||||
// Log entries (스크롤 위치 유지 — 자동 스크롤 안함)
|
||||
val logScrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 200.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.verticalScroll(logScrollState)
|
||||
) {
|
||||
for (entry in entries) {
|
||||
val color = when (entry.level) {
|
||||
|
||||
+83
-29
@@ -77,6 +77,10 @@ fun PlacementGuideView(appState: AppState) {
|
||||
|
||||
// 2단계 순차 탐색: VERTICAL → LATERAL → GREEN
|
||||
var placementPhase by remember { mutableStateOf(PlacementPhase.VERTICAL) }
|
||||
// 상하 탐색: 상한 경계 감지 로직
|
||||
var maxCenterSeen by remember { mutableIntStateOf(0) }
|
||||
var stableAtMax by remember { mutableIntStateOf(0) }
|
||||
var boundaryHit by remember { mutableStateOf(false) }
|
||||
|
||||
// 화면 진입 시 상태 리셋
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -511,20 +515,25 @@ fun PlacementGuideView(appState: AppState) {
|
||||
lastChannelData = channels
|
||||
lastWallResults = allWalls.toList()
|
||||
|
||||
// Detachment check (미부착 감지)
|
||||
// Detachment check (6채널 모두 수신됐을 때만)
|
||||
val fullCh = channels.filter { it.buffer.size >= 20 }
|
||||
val buffers = channels.sortedBy { it.channel }.map { it.buffer }
|
||||
val detachResult = DetachmentDetection.detect(buffers)
|
||||
val detachResult = if (fullCh.size >= 6) DetachmentDetection.detect(buffers) else null
|
||||
|
||||
if (false) { // detachment 비활성화
|
||||
directionHint = "Sensor detached — check contact"
|
||||
if (detachResult != null && detachResult.isDetached) {
|
||||
directionHint = "센서가 부착되지 않았습니다. 부착해주세요"
|
||||
directionIcon = "exclamationmark.triangle"
|
||||
placementScore = 0
|
||||
bleManager.sendLedMode(4) // DETACH_WARNING
|
||||
if (lastLedMode != 4) { bleManager.sendLedMode(4); lastLedMode = 4 }
|
||||
bleManager.debugLogger.warn("DETACHED mean=${"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||
Log.w("PlacementGuide", "DETACHED mean=${
|
||||
"%.1f".format(detachResult.meanAbs)} std=${"%.1f".format(detachResult.stdVal)}")
|
||||
} else {
|
||||
val guideResult = computePlacementGuide(detected, urineLens, allWalls, scanCount, placementPhase)
|
||||
val vs = VerticalState(maxCenterSeen, stableAtMax, boundaryHit)
|
||||
val guideResult = computePlacementGuide(detected, urineLens, allWalls, scanCount, placementPhase, vs)
|
||||
maxCenterSeen = vs.maxCenterSeen
|
||||
stableAtMax = vs.stableAtMax
|
||||
boundaryHit = vs.boundaryHit
|
||||
|
||||
// 디바운스: 같은 phase 2회 연속 + 최소 3초 유지
|
||||
if (guideResult.nextPhase == lastGuidePhase) {
|
||||
@@ -675,6 +684,24 @@ fun PlacementGuideView(appState: AppState) {
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Guide mode
|
||||
var useBoundary by remember { mutableStateOf(com.example.medilightv2android.managers.GreenZoneConstants.placementGuideMode == com.example.medilightv2android.managers.PlacementGuideMode.BOUNDARY) }
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Guide", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(if (useBoundary) "Boundary" else "Simple", fontSize = 13.sp, fontWeight = FontWeight.Bold,
|
||||
color = if (useBoundary) Color(0xFFFF5722) else MlPrimary)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Switch(checked = useBoundary, onCheckedChange = {
|
||||
useBoundary = it
|
||||
com.example.medilightv2android.managers.GreenZoneConstants.placementGuideMode =
|
||||
if (it) com.example.medilightv2android.managers.PlacementGuideMode.BOUNDARY
|
||||
else com.example.medilightv2android.managers.PlacementGuideMode.SIMPLE
|
||||
}, colors = SwitchDefaults.colors(checkedTrackColor = Color(0xFFFF5722)))
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Otsu toggle
|
||||
var otsuEnabled by remember { mutableStateOf(analyzer.useAdaptiveThreshold) }
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -898,12 +925,19 @@ private const val DEFAULT_LR_DEV_THR = 0.25
|
||||
* [3단계] 최종 Green: CV 확인
|
||||
* - CV ≤ threshold → Green (LED 6)
|
||||
*/
|
||||
data class VerticalState(
|
||||
var maxCenterSeen: Int = 0,
|
||||
var stableAtMax: Int = 0,
|
||||
var boundaryHit: Boolean = false
|
||||
)
|
||||
|
||||
private fun computePlacementGuide(
|
||||
detected: List<Boolean>,
|
||||
urineLens: List<Int>,
|
||||
walls: List<Pair<Int, Int>?> = emptyList(),
|
||||
repeatCount: Int = 0,
|
||||
currentPhase: PlacementPhase = PlacementPhase.VERTICAL
|
||||
currentPhase: PlacementPhase = PlacementPhase.VERTICAL,
|
||||
verticalState: VerticalState? = null
|
||||
): PlacementGuideResult {
|
||||
val detectedCount = detected.count { it }
|
||||
|
||||
@@ -912,7 +946,6 @@ private fun computePlacementGuide(
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
// Center 채널 정보
|
||||
val centerLens = (0..3).map { urineLens.getOrElse(it) { 0 } }
|
||||
val centerDetected = (0..3).count { detected.getOrElse(it) { false } }
|
||||
val ch0 = detected.getOrElse(0) { false }
|
||||
@@ -922,39 +955,60 @@ private fun computePlacementGuide(
|
||||
val len0 = centerLens[0]; val len1 = centerLens[1]
|
||||
val len2 = centerLens[2]; val len3 = centerLens[3]
|
||||
|
||||
// ═══ 1단계: 상하 탐색 (치골→위로→CH0 안잡히면 아래로) ═══
|
||||
// ═══ 1단계: 상하 탐색 ═══
|
||||
if (currentPhase == PlacementPhase.VERTICAL) {
|
||||
val guideMode = com.example.medilightv2android.managers.GreenZoneConstants.placementGuideMode
|
||||
val vs = verticalState ?: VerticalState()
|
||||
|
||||
// ── BOUNDARY 모드: 올라갔다 내려오기 ──
|
||||
if (guideMode == com.example.medilightv2android.managers.PlacementGuideMode.BOUNDARY) {
|
||||
if (centerDetected < 2) {
|
||||
vs.stableAtMax = 0
|
||||
val hint: String; val icon: String
|
||||
when {
|
||||
(ch3 || ch2) && !ch0 && !ch1 -> { hint = "센서를 아래로 천천히 내리세요 ↓"; icon = "arrow.down" }
|
||||
else -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" }
|
||||
}
|
||||
return PlacementGuideResult(hint, icon, centerDetected * 10,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
if (centerDetected >= vs.maxCenterSeen) { vs.maxCenterSeen = centerDetected; vs.stableAtMax++ }
|
||||
if (!vs.boundaryHit && vs.stableAtMax >= 2 && centerDetected < vs.maxCenterSeen) {
|
||||
vs.boundaryHit = true
|
||||
return PlacementGuideResult("너무 올라갔습니다. 아래로 내리세요 ↓", "arrow.down", 30,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
if (vs.boundaryHit) {
|
||||
if (centerDetected < vs.maxCenterSeen) {
|
||||
return PlacementGuideResult("조금 더 아래로 ↓", "arrow.down", 35,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
vs.boundaryHit = false; vs.stableAtMax = 0
|
||||
val l4 = urineLens.getOrElse(4) { 0 }; val l5 = urineLens.getOrElse(5) { 0 }
|
||||
val lrHint = when { l4 > 0 && l5 > 0 && l4 > l5 -> "최적 위치! 오른쪽으로 →"; l4 > 0 && l5 > 0 && l5 > l4 -> "최적 위치! 왼쪽으로 ←"; else -> "최적 위치! 좌우 조절하세요 ↔" }
|
||||
val lrIcon = when { l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.right"; l4 > 0 && l5 > 0 && l5 > l4 -> "arrow.left"; else -> "arrow.left.arrow.right" }
|
||||
return PlacementGuideResult(lrHint, lrIcon, 40, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL)
|
||||
}
|
||||
return PlacementGuideResult("센서를 위로 천천히 올리세요 ↑", "arrow.up", centerDetected * 10,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
// ── SIMPLE 모드: 3+채널 안정 → 바로 좌우 ──
|
||||
if (centerDetected < 3) {
|
||||
val hint: String
|
||||
val icon: String
|
||||
val hint: String; val icon: String
|
||||
when {
|
||||
// 위쪽만 잡힘 → 아래로 내리세요
|
||||
(ch3 || ch2) && !ch0 && !ch1 -> {
|
||||
hint = "센서를 아래로 천천히 내리세요 ↓"
|
||||
icon = "arrow.down"
|
||||
}
|
||||
// 아래쪽만 잡힘 → 위로 올리세요
|
||||
(ch0 || ch1) && !ch3 && !ch2 -> {
|
||||
hint = "센서를 위로 천천히 올리세요 ↑"
|
||||
icon = "arrow.up"
|
||||
}
|
||||
// 아무것도 또는 1개만 → 위로 올리세요 (치골에서 시작 기준)
|
||||
else -> {
|
||||
hint = "센서를 위로 천천히 올리세요 ↑"
|
||||
icon = "arrow.up"
|
||||
}
|
||||
(ch3 || ch2) && !ch0 && !ch1 -> { hint = "센서를 아래로 천천히 내리세요 ↓"; icon = "arrow.down" }
|
||||
(ch0 || ch1) && !ch3 && !ch2 -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" }
|
||||
else -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" }
|
||||
}
|
||||
return PlacementGuideResult(hint, icon, centerDetected * 10,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
// 3개 이상 검출 — CH0 유무 확인
|
||||
if (!ch0 && ch3) {
|
||||
return PlacementGuideResult("CH0이 약합니다. 센서를 아래로 조금 내리세요 ↓", "arrow.down", 25,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
// 크기 패턴 확인
|
||||
val validLens = (0..3).filter { detected.getOrElse(it) { false } }.map { centerLens[it] }
|
||||
val maxLen = validLens.maxOrNull() ?: 0
|
||||
val minLen = validLens.minOrNull() ?: 0
|
||||
|
||||
Reference in New Issue
Block a user