diff --git a/demo/src/main/kotlin/com/duooomi/ble/demo/DemoScreen.kt b/demo/src/main/kotlin/com/duooomi/ble/demo/DemoScreen.kt index eb9710b..ae2ec73 100644 --- a/demo/src/main/kotlin/com/duooomi/ble/demo/DemoScreen.kt +++ b/demo/src/main/kotlin/com/duooomi/ble/demo/DemoScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.duooomi.ble.DuooomiBleSDK +import com.duooomi.ble.core.BleRawJsonEvent import com.duooomi.ble.core.ConnectionState import com.duooomi.ble.models.PlayMode import kotlinx.coroutines.async @@ -111,6 +112,13 @@ fun DemoScreen( CDNHelper.cdnHost = sdk.config.normalizedCdnHost } + LaunchedEffect(sdk) { + sdk.rawJsonEvents.collect { evt -> + val arrow = if (evt.direction == BleRawJsonEvent.Direction.TX) "→ TX" else "← RX" + log("$arrow [${evt.commandName}] ${evt.json}") + } + } + Scaffold( topBar = { TopAppBar(title = { Text("SDK Demo") }) diff --git a/sdk/src/main/kotlin/com/duooomi/ble/DuooomiBleSDK.kt b/sdk/src/main/kotlin/com/duooomi/ble/DuooomiBleSDK.kt index c1199ae..9349919 100644 --- a/sdk/src/main/kotlin/com/duooomi/ble/DuooomiBleSDK.kt +++ b/sdk/src/main/kotlin/com/duooomi/ble/DuooomiBleSDK.kt @@ -54,6 +54,13 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) { private val _error = MutableStateFlow(null) val error: StateFlow = _error.asStateFlow() + /** + * BLE JSON 协议层原始收发事件流。**调试用**——日志/抓包/UI 联调展示, + * 不属业务行为契约(iOS 版无对应通道)。集成方不要依赖它做业务逻辑。 + */ + private val _rawJsonEvents = MutableSharedFlow(extraBufferCapacity = 32) + val rawJsonEvents: SharedFlow = _rawJsonEvents.asSharedFlow() + // MARK: - Internal Services private val bleClient = BleClient(context) @@ -85,6 +92,7 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) { init { BleLog.i("SDK initialized (apiHost=${config.apiHost}, brand=${config.brand})", "SDK") setupDisconnectHandler() + protocolService.rawJsonEmitter = { evt -> _rawJsonEvents.tryEmit(evt) } // 启动时把持久化记录回放到 StateFlow _upgradeRecords.value = upgradeRecordStore.all upgradeRecordStore.onChange = { records -> @@ -702,6 +710,7 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) { private fun handleIncomingMessage(commandType: Byte, data: ByteArray) { val typeValue = commandType.toInt() and 0xFF + emitRawJsonRx(typeValue, data) val opId = "$typeValue" // Check for prepareTransfer with key-based opId @@ -746,6 +755,26 @@ class DuooomiBleSDK(context: Context, val config: DuooomiBleConfig) { throw DuooomiBleError.BrandMismatch(expected = expected, actual = actual) } + private fun emitRawJsonRx(typeValue: Int, data: ByteArray) { + runCatching { + val cleaned = data.filter { it != 0.toByte() }.toByteArray() + if (cleaned.isEmpty()) return@runCatching + val jsonStr = String(cleaned, Charsets.UTF_8) + if (jsonStr.startsWith("{") || jsonStr.startsWith("[")) { + val name = CommandType.fromValue(typeValue)?.name + ?: "UNKNOWN(0x%02X)".format(typeValue) + _rawJsonEvents.tryEmit( + BleRawJsonEvent( + direction = BleRawJsonEvent.Direction.RX, + commandType = typeValue, + commandName = name, + json = jsonStr + ) + ) + } + } + } + private fun parseJsonResponse(data: ByteArray): JSONObject { // Device responses may contain null bytes val cleaned = data.filter { it != 0.toByte() }.toByteArray() diff --git a/sdk/src/main/kotlin/com/duooomi/ble/core/BleTypes.kt b/sdk/src/main/kotlin/com/duooomi/ble/core/BleTypes.kt index 236f72b..df187c1 100644 --- a/sdk/src/main/kotlin/com/duooomi/ble/core/BleTypes.kt +++ b/sdk/src/main/kotlin/com/duooomi/ble/core/BleTypes.kt @@ -12,6 +12,19 @@ data class DiscoveredDevice( val rssi: Int ) +/** + * BLE JSON 协议层的原始收发事件。**调试用**,不属业务行为契约—— + * 不要根据它做业务决策,仅用于日志/抓包/联调展示。iOS 版暂未提供等价通道。 + */ +data class BleRawJsonEvent( + val direction: Direction, + val commandType: Int, + val commandName: String, + val json: String +) { + enum class Direction { TX, RX } +} + sealed class DuooomiBleError(message: String) : Exception(message) { class BluetoothNotPoweredOn(state: String) : DuooomiBleError("Bluetooth not powered on: $state") object NotConnected : DuooomiBleError("No device connected") diff --git a/sdk/src/main/kotlin/com/duooomi/ble/services/BleProtocolService.kt b/sdk/src/main/kotlin/com/duooomi/ble/services/BleProtocolService.kt index 84aebbe..4d68ea0 100644 --- a/sdk/src/main/kotlin/com/duooomi/ble/services/BleProtocolService.kt +++ b/sdk/src/main/kotlin/com/duooomi/ble/services/BleProtocolService.kt @@ -1,6 +1,7 @@ package com.duooomi.ble.services import com.duooomi.ble.core.BleClient +import com.duooomi.ble.core.BleRawJsonEvent import com.duooomi.ble.core.DuooomiBleError import com.duooomi.ble.protocol.* import com.duooomi.ble.utils.BleLog @@ -21,6 +22,9 @@ internal class BleProtocolService(private val client: BleClient) { private val _incomingMessages = MutableSharedFlow>(extraBufferCapacity = 16) val incomingMessages: SharedFlow> = _incomingMessages + /** 调试用:DuooomiBleSDK 注入回调以接收 TX 原始 JSON 事件 */ + var rawJsonEmitter: ((BleRawJsonEvent) -> Unit)? = null + // MARK: - Start/Stop Listening fun startListening(scope: CoroutineScope) { @@ -74,7 +78,16 @@ internal class BleProtocolService(private val client: BleClient) { } suspend fun sendJSON(type: CommandType, payload: JSONObject) { - val jsonBytes = payload.toString().toByteArray(Charsets.UTF_8) + val jsonStr = payload.toString() + rawJsonEmitter?.invoke( + BleRawJsonEvent( + direction = BleRawJsonEvent.Direction.TX, + commandType = type.value, + commandName = type.name, + json = jsonStr + ) + ) + val jsonBytes = jsonStr.toByteArray(Charsets.UTF_8) send(type = type.byte, data = jsonBytes) }