Fix BLE companion transport reliability
This commit is contained in:
@@ -7,6 +7,7 @@ import android.bluetooth.le.ScanFilter
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.bluetooth.le.ScanSettings
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -33,6 +34,7 @@ class BleClient(private val context: Context) {
|
||||
private val incoming = StringBuilder()
|
||||
private val pending = ConcurrentHashMap<String, CompletableDeferred<JSONObject>>()
|
||||
private val writes = ArrayDeque<ByteArray>()
|
||||
private var writeInProgress = false
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var stopped = true
|
||||
|
||||
@@ -49,6 +51,7 @@ class BleClient(private val context: Context) {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
bluetoothManager.adapter?.bluetoothLeScanner?.stopScan(scanCallback)
|
||||
gatt?.close(); gatt = null; rx = null
|
||||
resetWrites()
|
||||
state.value = ConnectionState.STOPPED
|
||||
}
|
||||
|
||||
@@ -77,49 +80,119 @@ class BleClient(private val context: Context) {
|
||||
|
||||
private val gattCallback = object : BluetoothGattCallback() {
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||
if (newState == BluetoothProfile.STATE_CONNECTED) { this@BleClient.gatt = gatt; gatt.requestMtu(247); gatt.discoverServices() }
|
||||
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
|
||||
this@BleClient.gatt = gatt
|
||||
// Android GATT operations are serialized. Starting service
|
||||
// discovery while the MTU request is still pending can leave
|
||||
// the client in CONNECTING forever on some phones.
|
||||
if (!gatt.requestMtu(247)) gatt.discoverServices()
|
||||
}
|
||||
else {
|
||||
rx = null; gatt.close(); state.value = ConnectionState.DISCONNECTED
|
||||
rx = null; gatt.close(); resetWrites(); state.value = ConnectionState.DISCONNECTED
|
||||
if (!stopped) handler.postDelayed({ connect() }, 3_000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||
gatt.discoverServices()
|
||||
}
|
||||
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||
val service = gatt.getService(SERVICE_UUID) ?: return
|
||||
rx = service.getCharacteristic(RX_UUID)
|
||||
val tx = service.getCharacteristic(TX_UUID) ?: return
|
||||
gatt.setCharacteristicNotification(tx, true)
|
||||
tx.getDescriptor(CCC_UUID)?.let { descriptor ->
|
||||
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
val service = gatt.getService(SERVICE_UUID)
|
||||
val nextRx = service?.getCharacteristic(RX_UUID)
|
||||
val tx = service?.getCharacteristic(TX_UUID)
|
||||
val descriptor = tx?.getDescriptor(CCC_UUID)
|
||||
if (status != BluetoothGatt.GATT_SUCCESS || nextRx == null || tx == null || descriptor == null) {
|
||||
gatt.disconnect()
|
||||
return
|
||||
}
|
||||
rx = nextRx
|
||||
if (!gatt.setCharacteristicNotification(tx, true)) {
|
||||
gatt.disconnect()
|
||||
return
|
||||
}
|
||||
val descriptorStarted = if (Build.VERSION.SDK_INT >= 33) {
|
||||
gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) == BluetoothStatusCodes.SUCCESS
|
||||
} else {
|
||||
descriptor.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
|
||||
gatt.writeDescriptor(descriptor)
|
||||
}
|
||||
state.value = ConnectionState.CONNECTED
|
||||
if (!descriptorStarted) gatt.disconnect()
|
||||
}
|
||||
|
||||
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
|
||||
if (descriptor.uuid == CCC_UUID && status == BluetoothGatt.GATT_SUCCESS) {
|
||||
state.value = ConnectionState.CONNECTED
|
||||
} else {
|
||||
gatt.disconnect()
|
||||
}
|
||||
}
|
||||
@Deprecated("Used on Android 12 and earlier")
|
||||
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||
incoming.append(characteristic.value.toString(Charsets.UTF_8))
|
||||
while (incoming.contains("\n")) {
|
||||
val index = incoming.indexOf("\n")
|
||||
val line = incoming.substring(0, index); incoming.delete(0, index + 1)
|
||||
runCatching { JSONObject(line) }.getOrNull()?.let { response ->
|
||||
pending.remove(response.optString("id"))?.let { deferred ->
|
||||
if (response.optBoolean("ok")) deferred.complete(response.getJSONObject("data"))
|
||||
else deferred.completeExceptionally(IllegalStateException(response.optJSONObject("error")?.optString("message") ?: "Bluetooth request failed"))
|
||||
}
|
||||
handleIncoming(characteristic.value)
|
||||
}
|
||||
|
||||
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
|
||||
handleIncoming(value)
|
||||
}
|
||||
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) writeNext() else {
|
||||
synchronized(this@BleClient) { writeInProgress = false }
|
||||
gatt.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIncoming(value: ByteArray) {
|
||||
incoming.append(value.toString(Charsets.UTF_8))
|
||||
while (incoming.contains("\n")) {
|
||||
val index = incoming.indexOf("\n")
|
||||
val line = incoming.substring(0, index); incoming.delete(0, index + 1)
|
||||
runCatching { JSONObject(line) }.getOrNull()?.let { response ->
|
||||
pending.remove(response.optString("id"))?.let { deferred ->
|
||||
if (response.optBoolean("ok")) deferred.complete(response.getJSONObject("data"))
|
||||
else deferred.completeExceptionally(IllegalStateException(response.optJSONObject("error")?.optString("message") ?: "Bluetooth request failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { writeNext() }
|
||||
}
|
||||
|
||||
@Synchronized private fun enqueue(bytes: ByteArray) {
|
||||
bytes.asList().chunked(180).forEach { chunk -> writes.add(chunk.toByteArray()) }
|
||||
if (writes.size == bytes.asList().chunked(180).size) writeNext()
|
||||
val chunks = bytes.asList().chunked(180)
|
||||
chunks.forEach { chunk -> writes.add(chunk.toByteArray()) }
|
||||
if (!writeInProgress) {
|
||||
writeInProgress = true
|
||||
writeNext()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized private fun writeNext() {
|
||||
val characteristic = rx ?: return
|
||||
val next = writes.removeFirstOrNull() ?: return
|
||||
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||
characteristic.value = next
|
||||
gatt?.writeCharacteristic(characteristic)
|
||||
val characteristic = rx
|
||||
val currentGatt = gatt
|
||||
if (characteristic == null || currentGatt == null) {
|
||||
writeInProgress = false
|
||||
return
|
||||
}
|
||||
val next = writes.removeFirstOrNull()
|
||||
if (next == null) {
|
||||
writeInProgress = false
|
||||
return
|
||||
}
|
||||
val started = if (Build.VERSION.SDK_INT >= 33) {
|
||||
currentGatt.writeCharacteristic(characteristic, next, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) == BluetoothStatusCodes.SUCCESS
|
||||
} else {
|
||||
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||
characteristic.value = next
|
||||
currentGatt.writeCharacteristic(characteristic)
|
||||
}
|
||||
if (!started) {
|
||||
writeInProgress = false
|
||||
currentGatt.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized private fun resetWrites() {
|
||||
writes.clear()
|
||||
writeInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user