From 07d722809dbc5ab78af5b3f97dfc7ae92851b1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Sat, 28 Sep 2024 17:50:34 +0200 Subject: [PATCH 1/2] Add RPC Result characteristic - Add the RPC Result characteristic to the library's callbacks to allow using it - Increase MTU size to ensure the characteristic read is not truncated to 20 bytes --- .../com/wifi/improv/demo/ImprovViewModel.kt | 12 ++- .../java/com/wifi/improv/demo/MainActivity.kt | 10 ++- .../com/wifi/improv/BluetoothOperations.kt | 3 + .../java/com/wifi/improv/ImprovManager.kt | 90 +++++++++++++++++-- .../com/wifi/improv/ImprovManagerCallback.kt | 2 + 5 files changed, 105 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/wifi/improv/demo/ImprovViewModel.kt b/app/src/main/java/com/wifi/improv/demo/ImprovViewModel.kt index b8bc787..e840c1e 100644 --- a/app/src/main/java/com/wifi/improv/demo/ImprovViewModel.kt +++ b/app/src/main/java/com/wifi/improv/demo/ImprovViewModel.kt @@ -21,6 +21,7 @@ class ImprovViewModel : ViewModel(), ImprovManagerCallback { var connectedDevice: ImprovDevice? = null var deviceState: DeviceState? = null var errorState: ErrorState? = null + var rpcResult = listOf() private fun update() { viewModelScope.launch { @@ -31,7 +32,8 @@ class ImprovViewModel : ViewModel(), ImprovManagerCallback { connectedDevice?.address ?: "", connectedDevice != null, deviceState.toString(), - errorState.toString() + errorState.toString(), + rpcResult ) } } @@ -64,6 +66,11 @@ class ImprovViewModel : ViewModel(), ImprovManagerCallback { this.errorState = errorState update() } + + override fun onRpcResult(result: List) { + this.rpcResult = result + update() + } } @Immutable @@ -74,5 +81,6 @@ data class ImprovScreenState( val address: String, val btConnected: Boolean, val deviceState: String, - val errorState: String + val errorState: String, + val rpcResult: List ) \ No newline at end of file diff --git a/app/src/main/java/com/wifi/improv/demo/MainActivity.kt b/app/src/main/java/com/wifi/improv/demo/MainActivity.kt index c606b7b..d01929f 100644 --- a/app/src/main/java/com/wifi/improv/demo/MainActivity.kt +++ b/app/src/main/java/com/wifi/improv/demo/MainActivity.kt @@ -114,7 +114,8 @@ fun ImprovMain( name = screenState.name, address = screenState.address, deviceState = screenState.deviceState, - errorState = screenState.errorState + errorState = screenState.errorState, + rpcResult = screenState.rpcResult ) Spacer(Modifier.padding(5.dp)) } @@ -132,7 +133,8 @@ fun ImprovStatus( name: String?, address: String?, deviceState: String?, - errorState: String? + errorState: String?, + rpcResult: List ) { Card(modifier = Modifier.fillMaxWidth()) { Column { @@ -144,6 +146,7 @@ fun ImprovStatus( Text(text = "Address: $address") Text(text = "Device State: $deviceState") Text(text = "Error State: $errorState") + Text(text = "RPC Result: $rpcResult") } } } @@ -220,7 +223,8 @@ fun DefaultPreview() { "12:34:56:78", true, DeviceState.AUTHORIZED.toString(), - "errorState" + "errorState", + listOf() ) ImprovMain(screenState = improvScreenState, {}, {}, {}, { _, _ -> }) } \ No newline at end of file diff --git a/library/src/main/java/com/wifi/improv/BluetoothOperations.kt b/library/src/main/java/com/wifi/improv/BluetoothOperations.kt index b586ed1..0c47e10 100644 --- a/library/src/main/java/com/wifi/improv/BluetoothOperations.kt +++ b/library/src/main/java/com/wifi/improv/BluetoothOperations.kt @@ -9,6 +9,9 @@ sealed class BleOperationType data class Connect(val device: BluetoothDevice) : BleOperationType() data class Disconnect(val device: BluetoothDevice) : BleOperationType() +object DiscoverServices: BleOperationType() +object RequestLargeMtu: BleOperationType() + data class CharacteristicRead(val char: BluetoothGattCharacteristic) : BleOperationType() data class CharacteristicWrite(val char: BluetoothGattCharacteristic) : BleOperationType() diff --git a/library/src/main/java/com/wifi/improv/ImprovManager.kt b/library/src/main/java/com/wifi/improv/ImprovManager.kt index a514bd2..9f56b7f 100644 --- a/library/src/main/java/com/wifi/improv/ImprovManager.kt +++ b/library/src/main/java/com/wifi/improv/ImprovManager.kt @@ -25,7 +25,10 @@ class ImprovManager( UUID.fromString("00467768-6228-2272-4663-277478268001") private val UUID_CHAR_ERROR_STATE: UUID = UUID.fromString("00467768-6228-2272-4663-277478268002") - private val UUID_CHAR_RPC: UUID = UUID.fromString("00467768-6228-2272-4663-277478268003") + private val UUID_CHAR_RPC: UUID = + UUID.fromString("00467768-6228-2272-4663-277478268003") + private val UUID_CHAR_RPC_RESULT: UUID = + UUID.fromString("00467768-6228-2272-4663-277478268004") } private val bluetoothManager: BluetoothManager = @@ -63,8 +66,8 @@ class ImprovManager( ) ) - gatt.discoverServices() - + operationQueue.add(RequestLargeMtu) + operationQueue.add(DiscoverServices) } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Log.w(TAG, "Successfully disconnected from $deviceAddress") gatt.close() @@ -86,25 +89,35 @@ class ImprovManager( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { - val value = - characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0).toUByte() when (characteristic.uuid) { UUID_CHAR_CURRENT_STATE -> { + val value = + characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0).toUByte() Log.i(TAG, "Current State has changed to $value.") - val deviceState = DeviceState.values().firstOrNull { it.value == value } + val deviceState = DeviceState.entries.firstOrNull { it.value == value } if (deviceState != null) callback.onStateChange(deviceState) else Log.e(TAG, "Unable to determine Current State") } UUID_CHAR_ERROR_STATE -> { + val value = + characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0).toUByte() Log.i(TAG, "Error State has changed to $value.") - val errorState = ErrorState.values().firstOrNull { it.value == value } + val errorState = ErrorState.entries.firstOrNull { it.value == value } if (errorState != null) callback.onErrorStateChange(errorState) else Log.e(TAG, "Unable to determine Error State") } + UUID_CHAR_RPC_RESULT -> { + Log.i(TAG, "RPC Result has changed to ${characteristic.value.joinToString()}.") + val result = extractResultStrings(characteristic.value) + if (result != null) + callback.onRpcResult(result) + else + Log.w(TAG, "Received empty RPC Result") + } } } @@ -187,6 +200,26 @@ class ImprovManager( } } else Log.e(TAG, "Unable to register for Error State Notifications") + + val rpcResultChar = service.getCharacteristic(UUID_CHAR_RPC_RESULT) + enqueueOperation(CharacteristicRead(rpcResultChar)) + if (gatt.setCharacteristicNotification(rpcResultChar, true)) { + Log.i(TAG, "Registered for RPC Result Notifications") + rpcResultChar.descriptors.firstOrNull()?.let { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + enqueueOperation(DescriptorWrite(it)) + } + } else + Log.e(TAG, "Unable to register for RPC Result Notifications") + + if (pendingOperation is DiscoverServices) + signalEndOfOperation() + } + + override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { + Log.d(TAG, "MTU change to $mtu returned status: $status") + if (pendingOperation is RequestLargeMtu) + signalEndOfOperation() } } @@ -257,6 +290,35 @@ class ImprovManager( enqueueOperation(CharacteristicWrite(rpc)) } + private fun extractResultStrings(data: ByteArray): List? { + // Ensure the data is at least 3 bytes long to read the first string length + if (data.size < 3) return null + + val strings = mutableListOf() + var currentIndex = 2 // Start after the first two bytes + + while (currentIndex < data.size) { + // Get the length of the current string + val stringLength = data[currentIndex].toInt() + currentIndex++ + + // Ensure there are enough bytes left for the current string + if (currentIndex + stringLength > data.size) return strings + + // Extract the string and add it to the list + try { + val string = data.decodeToString(currentIndex, currentIndex + stringLength, throwOnInvalidSequence = true) + currentIndex += stringLength + strings += string + } catch (e: Exception) { + Log.e(TAG, "Invalid string encoding, returning strings previously decoded") + return strings + } + } + + return strings + } + private val operationQueue = ConcurrentLinkedQueue() private var pendingOperation: BleOperationType? = null @@ -288,6 +350,13 @@ class ImprovManager( is Disconnect -> { // Noop? } + is DiscoverServices -> { + if (bluetoothGatt != null) { + bluetoothGatt!!.discoverServices() + } else { + Log.e(TAG, "Tried to discover services without device connected.") + } + } is CharacteristicWrite -> { if (bluetoothGatt != null) { bluetoothGatt!!.writeCharacteristic(operation.char) @@ -309,6 +378,13 @@ class ImprovManager( Log.e(TAG, "Tried writing descriptor without device connected.") } } + is RequestLargeMtu -> { + if (bluetoothGatt != null) { + bluetoothGatt!!.requestMtu(517) + } else { + Log.e(TAG, "Tried requesting MTU without device connected.") + } + } else -> { error("Unhandled Operation!") } diff --git a/library/src/main/java/com/wifi/improv/ImprovManagerCallback.kt b/library/src/main/java/com/wifi/improv/ImprovManagerCallback.kt index 3768b59..f1abe9d 100644 --- a/library/src/main/java/com/wifi/improv/ImprovManagerCallback.kt +++ b/library/src/main/java/com/wifi/improv/ImprovManagerCallback.kt @@ -11,4 +11,6 @@ interface ImprovManagerCallback { fun onStateChange(state: DeviceState) fun onErrorStateChange(errorState: ErrorState) + + fun onRpcResult(result: List) } \ No newline at end of file From 2762411d45f0194a9d9017add5bf7bf341942a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Sat, 28 Sep 2024 17:59:04 +0200 Subject: [PATCH 2/2] Undo Kotlin 1.9 feature --- library/src/main/java/com/wifi/improv/ImprovManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/wifi/improv/ImprovManager.kt b/library/src/main/java/com/wifi/improv/ImprovManager.kt index 9f56b7f..e4522e5 100644 --- a/library/src/main/java/com/wifi/improv/ImprovManager.kt +++ b/library/src/main/java/com/wifi/improv/ImprovManager.kt @@ -94,7 +94,7 @@ class ImprovManager( val value = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0).toUByte() Log.i(TAG, "Current State has changed to $value.") - val deviceState = DeviceState.entries.firstOrNull { it.value == value } + val deviceState = DeviceState.values().firstOrNull { it.value == value } if (deviceState != null) callback.onStateChange(deviceState) else @@ -104,7 +104,7 @@ class ImprovManager( val value = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0).toUByte() Log.i(TAG, "Error State has changed to $value.") - val errorState = ErrorState.entries.firstOrNull { it.value == value } + val errorState = ErrorState.values().firstOrNull { it.value == value } if (errorState != null) callback.onErrorStateChange(errorState) else