feat: ADC 로그 저장 + 기기정보 mid? + LED 모드 + 무한 3초 reconnect

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 10:55:29 +09:00
parent fde4d0ad05
commit e6e339379b
3 changed files with 50 additions and 22 deletions
@@ -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")
@@ -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+)")
@@ -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
}
}