From 8b182921001442915864f0d2161f421d743489df Mon Sep 17 00:00:00 2001 From: km2023 Date: Wed, 22 Apr 2026 17:01:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=87=8D=E8=BF=9E?= =?UTF-8?q?=E5=90=8E=20BLE=20write=20=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20+=20=E6=94=AF=E6=8C=81=20composite=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - connect 前清理遗留 gatt,避免实例泄漏 - 等 CCCD descriptor 写入完成再 resume 连接,防止首次 write 卡在 gatt 串行队列 - writeWithoutResponse 失败后重试一次,仍失败抛 WriteFailed,让上层 RPC 快速失败而非等 10 秒超时 - sdk/build.gradle.kts 声明 group/version 以便 QA app composite build 引用 Co-Authored-By: Claude Opus 4.7 --- sdk/build.gradle.kts | 3 ++ .../kotlin/com/duooomi/ble/core/BleClient.kt | 54 +++++++++++++++---- .../kotlin/com/duooomi/ble/core/BleTypes.kt | 1 + 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/sdk/build.gradle.kts b/sdk/build.gradle.kts index 0d7beb7..2ef9688 100644 --- a/sdk/build.gradle.kts +++ b/sdk/build.gradle.kts @@ -3,6 +3,9 @@ plugins { id("org.jetbrains.kotlin.android") } +group = "com.duooomi" +version = "0.0.0-local" + android { namespace = "com.duooomi.ble" compileSdk = 34 diff --git a/sdk/src/main/kotlin/com/duooomi/ble/core/BleClient.kt b/sdk/src/main/kotlin/com/duooomi/ble/core/BleClient.kt index ae637a2..30e9e52 100644 --- a/sdk/src/main/kotlin/com/duooomi/ble/core/BleClient.kt +++ b/sdk/src/main/kotlin/com/duooomi/ble/core/BleClient.kt @@ -134,6 +134,16 @@ internal class BleClient(context: Context) { throw DuooomiBleError.BluetoothNotPoweredOn("Bluetooth is off") } + connectedGatt?.let { old -> + BleLog.i("Stale gatt present; cleaning up before reconnect", "BLE") + runCatching { old.disconnect() } + runCatching { old.close() } + connectedGatt = null + connectedDeviceId = null + writeCharacteristic = null + readCharacteristic = null + } + val device = bluetoothAdapter?.getRemoteDevice(deviceId) ?: throw DuooomiBleError.ConnectionFailed("Device not found: $deviceId") @@ -185,12 +195,17 @@ internal class BleClient(context: Context) { writeMutex.withLock { characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE characteristic.value = data - val success = gatt.writeCharacteristic(characteristic) + var success = gatt.writeCharacteristic(characteristic) if (!success) { - BleLog.w("Write failed, len=${data.size}", "BLE") - } else { - BleLog.d("Write w/o response, len=${data.size}", "BLE") + BleLog.w("Write failed; retry once after 30ms, len=${data.size}", "BLE") + delay(30) + success = gatt.writeCharacteristic(characteristic) } + if (!success) { + BleLog.e("Write failed after retry, len=${data.size}", "BLE") + throw DuooomiBleError.WriteFailed("Gatt busy; write rejected") + } + BleLog.d("Write w/o response, len=${data.size}", "BLE") } } @@ -294,6 +309,9 @@ internal class BleClient(context: Context) { return } + connectedGatt = gatt + connectedDeviceId = gatt.device.address + // Enable notifications on read characteristic val readChar = readCharacteristic!! gatt.setCharacteristicNotification(readChar, true) @@ -302,16 +320,32 @@ internal class BleClient(context: Context) { ) if (descriptor != null) { descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - gatt.writeDescriptor(descriptor) + val ok = gatt.writeDescriptor(descriptor) + if (ok) { + // Resume in onDescriptorWrite, after the CCCD write is flushed. + return + } + BleLog.w("writeDescriptor returned false; resuming anyway", "BLE") } + resumeConnect(gatt) + } - // Connection fully ready - connectedGatt = gatt - connectedDeviceId = gatt.device.address - val cont = connectContinuation + override fun onDescriptorWrite( + gatt: BluetoothGatt, + descriptor: BluetoothGattDescriptor, + status: Int + ) { + BleLog.d("Descriptor written: uuid=${descriptor.uuid}, status=$status", "BLE") + if (descriptor.uuid == java.util.UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) { + resumeConnect(gatt) + } + } + + private fun resumeConnect(gatt: BluetoothGatt) { + val cont = connectContinuation ?: return connectContinuation = null BleLog.i("Characteristics ready; connection established", "BLE") - cont?.resume(gatt) + cont.resume(gatt) } @Suppress("DEPRECATION") 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 a757498..76a2415 100644 --- a/sdk/src/main/kotlin/com/duooomi/ble/core/BleTypes.kt +++ b/sdk/src/main/kotlin/com/duooomi/ble/core/BleTypes.kt @@ -24,6 +24,7 @@ sealed class DuooomiBleError(message: String) : Exception(message) { class DeleteFileFailed(val status: Int) : DuooomiBleError("Delete failed: status=$status") class PrepareTransferFailed(val status: String) : DuooomiBleError("Prepare failed: $status") class TransferFailed(reason: String) : DuooomiBleError("Transfer failed: $reason") + class WriteFailed(reason: String) : DuooomiBleError("Write failed: $reason") class UnsupportedFormat(val format: String) : DuooomiBleError("Unsupported file format: $format. Supported: jpeg, png, gif, mp4, webm, webp") object InvalidFrame : DuooomiBleError("Invalid protocol frame") object PermissionDenied : DuooomiBleError("Bluetooth permission denied")