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