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:
@@ -3,6 +3,9 @@ plugins {
|
|||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group = "com.duooomi"
|
||||||
|
version = "0.0.0-local"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.duooomi.ble"
|
namespace = "com.duooomi.ble"
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|||||||
@@ -134,6 +134,16 @@ internal class BleClient(context: Context) {
|
|||||||
throw DuooomiBleError.BluetoothNotPoweredOn("Bluetooth is off")
|
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)
|
val device = bluetoothAdapter?.getRemoteDevice(deviceId)
|
||||||
?: throw DuooomiBleError.ConnectionFailed("Device not found: $deviceId")
|
?: throw DuooomiBleError.ConnectionFailed("Device not found: $deviceId")
|
||||||
|
|
||||||
@@ -185,12 +195,17 @@ internal class BleClient(context: Context) {
|
|||||||
writeMutex.withLock {
|
writeMutex.withLock {
|
||||||
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
||||||
characteristic.value = data
|
characteristic.value = data
|
||||||
val success = gatt.writeCharacteristic(characteristic)
|
var success = gatt.writeCharacteristic(characteristic)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
BleLog.w("Write failed, len=${data.size}", "BLE")
|
BleLog.w("Write failed; retry once after 30ms, len=${data.size}", "BLE")
|
||||||
} else {
|
delay(30)
|
||||||
BleLog.d("Write w/o response, len=${data.size}", "BLE")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedGatt = gatt
|
||||||
|
connectedDeviceId = gatt.device.address
|
||||||
|
|
||||||
// Enable notifications on read characteristic
|
// Enable notifications on read characteristic
|
||||||
val readChar = readCharacteristic!!
|
val readChar = readCharacteristic!!
|
||||||
gatt.setCharacteristicNotification(readChar, true)
|
gatt.setCharacteristicNotification(readChar, true)
|
||||||
@@ -302,16 +320,32 @@ internal class BleClient(context: Context) {
|
|||||||
)
|
)
|
||||||
if (descriptor != null) {
|
if (descriptor != null) {
|
||||||
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
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
|
override fun onDescriptorWrite(
|
||||||
connectedGatt = gatt
|
gatt: BluetoothGatt,
|
||||||
connectedDeviceId = gatt.device.address
|
descriptor: BluetoothGattDescriptor,
|
||||||
val cont = connectContinuation
|
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
|
connectContinuation = null
|
||||||
BleLog.i("Characteristics ready; connection established", "BLE")
|
BleLog.i("Characteristics ready; connection established", "BLE")
|
||||||
cont?.resume(gatt)
|
cont.resume(gatt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ sealed class DuooomiBleError(message: String) : Exception(message) {
|
|||||||
class DeleteFileFailed(val status: Int) : DuooomiBleError("Delete failed: status=$status")
|
class DeleteFileFailed(val status: Int) : DuooomiBleError("Delete failed: status=$status")
|
||||||
class PrepareTransferFailed(val status: String) : DuooomiBleError("Prepare failed: $status")
|
class PrepareTransferFailed(val status: String) : DuooomiBleError("Prepare failed: $status")
|
||||||
class TransferFailed(reason: String) : DuooomiBleError("Transfer failed: $reason")
|
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")
|
class UnsupportedFormat(val format: String) : DuooomiBleError("Unsupported file format: $format. Supported: jpeg, png, gif, mp4, webm, webp")
|
||||||
object InvalidFrame : DuooomiBleError("Invalid protocol frame")
|
object InvalidFrame : DuooomiBleError("Invalid protocol frame")
|
||||||
object PermissionDenied : DuooomiBleError("Bluetooth permission denied")
|
object PermissionDenied : DuooomiBleError("Bluetooth permission denied")
|
||||||
|
|||||||
Reference in New Issue
Block a user