fix: 修复重连后 BLE write 失败问题 + 支持 composite build

- 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 <noreply@anthropic.com>
This commit is contained in:
km2023
2026-04-22 17:01:21 +08:00
parent ed34c59c35
commit 8b18292100
3 changed files with 48 additions and 10 deletions

View File

@@ -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

View File

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

View File

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