From e6e339379b28a69534aad683ada59d4879fabeca Mon Sep 17 00:00:00 2001 From: jjangddu Date: Fri, 24 Apr 2026 10:55:29 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20ADC=20=EB=A1=9C=EA=B7=B8=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20+=20=EA=B8=B0=EA=B8=B0=EC=A0=95=EB=B3=B4=20mid=3F?= =?UTF-8?q?=20+=20LED=20=EB=AA=A8=EB=93=9C=20+=20=EB=AC=B4=ED=95=9C=203?= =?UTF-8?q?=EC=B4=88=20reconnect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. ADC 로그: 측정 완료 시 MeasurementLogService.logMeasurement() 호출 → 6채널 raw ADC + 채널별 ant/post/score 저장 (JSONL) 2. 기기 정보: 연결 직후 mid? 전송 → rid: 응답 파싱 → 로그 기록 (HW Version, Serial Number, FW Version 일괄 조회) 3. LED 모드: Placement Guide 연동 - 스캔 시작 → mls?(5) ALIGN_SEARCHING - PASS → mls?(6) ALIGN_COMPLETE - 스캔 종료 → mls?(0) OFF 4. Reconnect: 3/5/10초 5회 → 3초 간격 무한 재시도 (연결됐다가 끊어진 경우, 사용자가 Home 돌아갈 때까지) 5. BLE 로그: mid/mls TX 파싱, rid:/rls: RX 파싱 추가 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../medilightv2android/ble/BleManager.kt | 46 +++++++++++-------- .../views/monitoring/PiezoMonitoringView.kt | 15 +++++- .../ui/views/monitoring/PlacementGuideView.kt | 11 ++++- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt index 5d5e78e..74ed71f 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -60,8 +60,7 @@ class BleManager private constructor(private val context: Context) { var lastConnectedAddress: String? = null private set private var reconnectAttempt = 0 - private val maxReconnectAttempts = 5 - private val reconnectDelays = longArrayOf(3000, 5000, 10000, 10000, 10000) + private val reconnectIntervalMs: Long = 3000 private var reconnectTimer: Runnable? = null private var lastRxTimestamp: Long = 0 private var watchdogTimer: Runnable? = null @@ -281,6 +280,8 @@ class BleManager private constructor(private val context: Context) { "mqq" -> "NIRS activate" "mag" -> "NIRS gain query" "mcj" -> "NIRS MCJ query" + "mid" -> "device info query" + "mls" -> "LED mode ${if (data.size > 4) data[4].toInt() and 0xFF else "?"}" else -> "" } logd { "sendRaw: $cmdPreview (${data.size} bytes)" } @@ -344,6 +345,14 @@ class BleManager private constructor(private val context: Context) { sendRaw(CRC16.buildCommand("msn", intArrayOf(0))) } + fun sendDeviceInfoQuery() { + sendRaw(CRC16.buildCommand("mid")) + } + + fun sendLedMode(state: Int) { + sendRaw(CRC16.buildCommand("mls", intArrayOf(state))) + } + fun startBatteryPolling() { batteryTimer?.let { handler.removeCallbacks(it) } sendBatteryQuery() @@ -437,23 +446,14 @@ class BleManager private constructor(private val context: Context) { // ── Auto Reconnect ── private fun scheduleAutoReconnect() { - if (reconnectAttempt >= maxReconnectAttempts) { - logw { "Max reconnect attempts ($maxReconnectAttempts) reached" } - debugLogger.reconnectFailed() - isReconnecting.value = false - onReconnectionFailed?.invoke() - return - } - - val delay = reconnectDelays[reconnectAttempt.coerceAtMost(reconnectDelays.size - 1)] - logd { "Auto-reconnect attempt ${reconnectAttempt + 1}/$maxReconnectAttempts in ${delay}ms" } - debugLogger.reconnecting(reconnectAttempt + 1, maxReconnectAttempts, delay) + logd { "Auto-reconnect attempt ${reconnectAttempt + 1} in ${reconnectIntervalMs}ms" } + debugLogger.reconnecting(reconnectAttempt + 1, -1, reconnectIntervalMs) isReconnecting.value = true cancelReconnectTimer() val timer = Runnable { attemptAutoReconnect() } reconnectTimer = timer - handler.postDelayed(timer, delay) + handler.postDelayed(timer, reconnectIntervalMs) } private var reconnectScanCallback: ScanCallback? = null @@ -522,13 +522,9 @@ class BleManager private constructor(private val context: Context) { try { scanner.stopScan(callback) } catch (_: Exception) {} reconnectScanCallback = null - if (!isConnected.value && reconnectAttempt < maxReconnectAttempts) { - debugLogger.warn("RECONNECT_SCAN_TIMEOUT #$reconnectAttempt — device not found") + if (!isConnected.value) { + debugLogger.warn("RECONNECT_SCAN_TIMEOUT #$reconnectAttempt — device not found, retrying...") scheduleAutoReconnect() - } else if (!isConnected.value) { - debugLogger.reconnectFailed() - isReconnecting.value = false - onReconnectionFailed?.invoke() } }, 8000) } @@ -699,6 +695,7 @@ class BleManager private constructor(private val context: Context) { isServiceReady.value = true startBatteryPolling() startWatchdog() + sendDeviceInfoQuery() logd { "Service ready! (CCCD written)" } } } else { @@ -783,6 +780,15 @@ class BleManager private constructor(private val context: Context) { debugLogger.rx("rsn", data.size, "battery=${millivolts}mV (${pct}%)") } } + "rid:" -> { + val info = if (data.size > 4) String(data, 4, data.size - 4, Charsets.US_ASCII).trim() else "?" + debugLogger.rx("rid", data.size, "device_info=$info") + debugLogger.info("DEVICE_INFO $info") + } + "rls:" -> { + val state = if (data.size >= 5) data[4].toInt() and 0xFF else -1 + debugLogger.rx("rls", data.size, "led_state=$state") + } else -> { val hex = data.take(8).joinToString(" ") { "%02X".format(it) } debugLogger.rx("???", data.size, "unknown prefix='$prefix' hex=$hex") diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index d4ac6a3..51c7656 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -130,8 +130,20 @@ fun PiezoMonitoringView(appState: AppState) { } } + // ADC 로그 저장 (매 측정마다) + val logService = com.example.medilightv2android.services.MeasurementLogService.getInstance(context) + val channelLogs = analysisResult.channels.map { ch -> + com.example.medilightv2android.services.ChannelLogEntry( + channel = ch.channel, + ant = ch.result?.ant, + post = ch.result?.post, + urineLen = ch.result?.urineLen, + score = ch.result?.score + ) + } + val rawADC = channels.sortedBy { it.channel }.map { it.buffer } + if (centerWallCount >= 3) { - // 6ch BV estimation (center 4ch + lateral lr_ratio) val bvResult = estimateBladderVolume6ch(allWalls) if (bvResult != null) { lastVolumeMl = bvResult.volumeMl @@ -139,6 +151,7 @@ fun PiezoMonitoringView(appState: AppState) { Log.d("PiezoMonitor", "BV: ${"%.1f".format(bvResult.volumeMl)}ml (6ch, lr=${"%.2f".format(bvResult.lrRatio)}, center=${centerWallCount}/4)") bleManager.debugLogger.measurementResult(bvResult.volumeMl, bvResult.lrRatio, totalWallCount, (0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 }) + logService.logMeasurement(bvResult.volumeMl, channelLogs, rawADC) } } else if (centerWallCount > 0) { Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)") diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index 5e2d14a..59daf37 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -338,14 +338,23 @@ fun PlacementGuideView(appState: AppState) { bleManager.sendAllChannels() } - // Continuous scan loop (3초 간격) + // Continuous scan loop (3초 간격) + LED 모드 LaunchedEffect(isContinuousMode) { if (isContinuousMode) { + bleManager.sendLedMode(5) // ALIGN_SEARCHING performScan() while (isContinuousMode) { kotlinx.coroutines.delay(3000) if (isContinuousMode && !isScanning) performScan() } + bleManager.sendLedMode(0) // OFF + } + } + + // PASS 시 LED 완료 표시 + LaunchedEffect(isLocked) { + if (isLocked) { + bleManager.sendLedMode(6) // ALIGN_COMPLETE } }