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")