feat: BLE 디버그 로거 — 펌웨어 디버깅용 상세 통신 로그
BleDebugLogger 신규: - TX/RX 패킷 기록 (타임스탬프 ms, 데이터 크기) - RSSI 실시간 추적 (5초마다 readRemoteRssi) - 연결/끊김/재연결 이벤트 + GATT status code - 패킷 타임아웃, 채널 누락, Endian 감지 로그 - 측정 결과 (volumeMl, lrRatio, validChannels) - 파일 내보내기 (.log / .jsonl) + 클립보드 복사 - 세션 통계 (TX/RX/ERR/RSSI avg/min/max) BleManager 연동: - sendRaw() → TX 로그 - processReceivedData() → RX 로그 - connected/disconnected/reconnecting/reconnectFailed 이벤트 - onReadRemoteRssi 콜백 → RSSI 기록 PiezoMonitoringView: - Developer Tools 하단에 BLE Debug Panel 추가 - 2초 자동 갱신, 색상 코딩 (TX=파랑, RX=초록, ERR=빨강, RSSI=노랑, CONN=보라) - 클립보드 복사 / 파일 저장 / 클리어 버튼 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
private var txCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private var rxCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
val debugLogger = com.example.medilightv2android.services.BleDebugLogger.getInstance(context)
|
||||
private var scanTimer: Runnable? = null
|
||||
private var batteryTimer: Runnable? = null
|
||||
private var connectionTimer: Runnable? = null
|
||||
@@ -265,8 +266,9 @@ class BleManager private constructor(private val context: Context) {
|
||||
loge { "sendRaw: bluetoothGatt is null!" }
|
||||
return
|
||||
}
|
||||
val cmdPreview = if (data.size >= 4) String(data, 0, 4, Charsets.US_ASCII) else "?"
|
||||
val cmdPreview = if (data.size >= 4) String(data, 0, 4, Charsets.US_ASCII).trimEnd('?') else "?"
|
||||
logd { "sendRaw: $cmdPreview (${data.size} bytes)" }
|
||||
debugLogger.tx("$cmdPreview (${data.size}B)", data.size)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val result = gatt.writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE)
|
||||
logd { "sendRaw result: $result" }
|
||||
@@ -329,9 +331,11 @@ class BleManager private constructor(private val context: Context) {
|
||||
fun startBatteryPolling() {
|
||||
batteryTimer?.let { handler.removeCallbacks(it) }
|
||||
sendBatteryQuery()
|
||||
bluetoothGatt?.readRemoteRssi()
|
||||
val poll = object : Runnable {
|
||||
override fun run() {
|
||||
sendBatteryQuery()
|
||||
bluetoothGatt?.readRemoteRssi()
|
||||
handler.postDelayed(this, 5000)
|
||||
}
|
||||
}
|
||||
@@ -349,6 +353,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
private fun scheduleAutoReconnect() {
|
||||
if (reconnectAttempt >= maxReconnectAttempts) {
|
||||
logw { "Max reconnect attempts ($maxReconnectAttempts) reached" }
|
||||
debugLogger.reconnectFailed()
|
||||
isReconnecting.value = false
|
||||
onReconnectionFailed?.invoke()
|
||||
return
|
||||
@@ -356,6 +361,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
|
||||
val delay = reconnectDelays[reconnectAttempt.coerceAtMost(reconnectDelays.size - 1)]
|
||||
logd { "Auto-reconnect attempt ${reconnectAttempt + 1}/$maxReconnectAttempts in ${delay}ms" }
|
||||
debugLogger.reconnecting(reconnectAttempt + 1, maxReconnectAttempts, delay)
|
||||
isReconnecting.value = true
|
||||
|
||||
cancelReconnectTimer()
|
||||
@@ -442,6 +448,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
lastConnectedAddress = gatt.device.address
|
||||
reconnectAttempt = 0
|
||||
cancelReconnectTimer()
|
||||
debugLogger.connected(connectedDeviceName.value, gatt.device.address)
|
||||
|
||||
try {
|
||||
val prefs = context.getSharedPreferences("medilight_prefs", Context.MODE_PRIVATE)
|
||||
@@ -468,6 +475,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
onConnectionStateChanged?.invoke(false)
|
||||
|
||||
if (!isUserDisconnect && lastConnectedAddress != null) {
|
||||
debugLogger.disconnected("unexpected", status)
|
||||
onUnexpectedDisconnect?.invoke()
|
||||
scheduleAutoReconnect()
|
||||
}
|
||||
@@ -562,19 +570,18 @@ class BleManager private constructor(private val context: Context) {
|
||||
handler.post { processReceivedData(data) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
handler.post { debugLogger.rssi(rssi) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processReceivedData(data: ByteArray) {
|
||||
val prefix = String(data, 0, minOf(4, data.size), Charsets.US_ASCII)
|
||||
logd { "Received: $prefix (${data.size} bytes), callback=${
|
||||
when {
|
||||
prefix.startsWith("rcj") -> "onNirsMcjReceived=${onNirsMcjReceived != null}"
|
||||
prefix.startsWith("rta") -> "onNirsPowerOnReceived=${onNirsPowerOnReceived != null}"
|
||||
prefix.startsWith("rqq") || prefix.startsWith("rsh") -> "onNirsSensorActivated=${onNirsSensorActivated != null}"
|
||||
prefix.startsWith("rag") -> "onNirsMagReceived=${onNirsMagReceived != null}"
|
||||
else -> "n/a"
|
||||
}
|
||||
}" }
|
||||
debugLogger.rx(prefix.trimEnd(':', '!'), data.size)
|
||||
logd { "Received: $prefix (${data.size} bytes)" }
|
||||
when (prefix) {
|
||||
"rta:", "rta!" -> onNirsPowerOnReceived?.invoke()
|
||||
"rsh:", "rsh!", "rqq:", "rqq!" -> onNirsSensorActivated?.invoke()
|
||||
|
||||
Reference in New Issue
Block a user