Bluetooth Low Energy on Android, Part 2
AndroidIn (https://nerdranchighq.wpengine.com/blog/bluetooth-low-energy-part-1/) we set up our BLE Server and Client and were able to connect them. Now it's time to take a look...
And now, the long-awaited third part of my Bluetooth Low Energy on Android series!!
In Part 2 we were able to successfully send a message from the Client to the Server and have the Server echo it back. In this final installment, we will dive into Descriptors. We will discuss their usage and how to set one up to handle the Server pushing Characteristic updates to the Client.
Characteristic Descriptors are defined attributes that describe a characteristic or its value. While there are a lot of descriptors, the most commonly used is the Client Characteristic Configuration Descriptor. This Descriptor specifies how the parent Characteristic can be configured by a client device. In this case, we’ll be using it to control notifications. As a reminder, here is an overview of the GATT structure.
But you may ask “Why use a Descriptor to control Characteristic notifications? We already set those up in part 2.” That’s true! Essentially, this is an extra step that ensures the Server doesn’t send a notification to a Client that is in the incorrect state to read values from the Characteristic. Depending on the application, this may not mean anything more than a dropped message but could be important to the protocol to delay notifications until the Client has finished setup. Note: In the time since part 2 was released, Android now has full support for Kotlin. The app has been converted to take advantage of all the new bells and whistles.
Let’s start by adding a button to send a timestamp from the Server to the Client.
override fun onCreate(savedInstanceState: Bundle?) { ... with(binding) { sendTimestampButton.setOnClickListener { sendTimestamp() } } }
Then, create the Client Configuration Descriptor for sending the timestamp, and add to a new Characteristic.
private fun setupServer() { ... val writeCharacteristic = BluetoothGattCharacteristic(...) val clientConfigurationDescriptor = BluetoothGattDescriptor( CLIENT_CONFIGURATION_DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE).apply { value = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE } val notifyCharacteristic = BluetoothGattCharacteristic( CHARACTERISTIC_TIME_UUID, properties = 0, permissions = 0) notifyCharacteristic.addDescriptor(clientConfigurationDescriptor) ... }
As a reminder, the Characteristic UUIDs are created from a string of the format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
and can be any unique string you choose. However, the Client Configuration Descriptor has a 16 bit “short UUID” of 0x2902
and is masked against a Base UUID of 0000xxxx-0000-1000-8000-00805F9B34FB
to create full 128 bit UUID. The Bluetooth® Service Discovery Protocol (SDP) specification defines a way to represent a range of UUIDs (which are nominally 128 bits) in a shorter form.
Now that we have our Characteristic with Descriptor, add it to the gattServer
like usual.
private fun setupServer() { ... val notifyCharacteristic = BluetoothGattCharacteristic(...).apply{...} gattServer!!.addService( BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY).apply { addCharacteristic(writeCharacteristic) addCharacteristic(notifyCharacteristic) } ) }
Switching to our Client, let’s have it look for our new Characteristic and enable notifications once we’ve discovered services.
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { ... gatt.services.find { it.uuid == SERVICE_UUID } ?.characteristics?.find { it.uuid == CHARACTERISTIC_TIME_UUID } ?.let { if (gatt.setCharacteristicNotification(it, true)) { enableCharacteristicConfigurationDescriptor(gatt, it) } } }
This step is similar to how we set the write type for the Echo Characteristc, but now we also enable the Client Configuration Descriptor. To find it we can either look for the short UUID, or the full 128 bit UUID created by masking the short UUID with the Base UUID.
private fun enableCharacteristicConfigurationDescriptor(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { characteristic.descriptors.find { it.uuid.toString().substring(4, 8) == CLIENT_CONFIGURATION_DESCRIPTOR_ID } ?.apply { value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(this) } }
If we wanted to check if the Descriptor was enabled correctly, we could implement onDescriptorWrite
to check if we received a GATT_SUCCESS
for the status
.
Now in our Server, we need to add the Clients that are listening to the Configuration Descriptor to our clientConfigurations
map. This signifies that the Client is connected and has correctly configured the Descriptor for use, which we will check when notifying them.
override fun onDescriptorWriteRequest(device: BluetoothDevice, requestId: Int, descriptor: BluetoothGattDescriptor, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value) if (CLIENT_CONFIGURATION_DESCRIPTOR_UUID == descriptor.uuid) { clientConfigurations[device.address] = value sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) } }
Next, we build a simple timestamp string, convert it to bytes, and send it to notifyCharacteristic
.
private val timestampBytes: ByteArray get() { @SuppressLint("SimpleDateFormat") val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") val timestamp = dateFormat.format(Date()) return timestamp.toByteArray() } private fun sendTimestamp() = notifyCharacteristic(timestampBytes, CHARACTERISTIC_TIME_UUID)
Since we now have multiple Characteristics (one for echoing a reversed message and the new one to send a timestamp) to notify, let’s extract the logic and make it reusable. First, we find the Service then Characteristic and set its value. Then, check if the Characteristic needs confirmation by checking its properties. Note: Indications require confirmation, notifications do not.
private fun notifyCharacteristic(value: ByteArray, uuid: UUID) { handler.post { gattServer?.getService(SERVICE_UUID) ?.getCharacteristic(uuid)?.let { it.value = value val confirm = it.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE == BluetoothGattCharacteristic.PROPERTY_INDICATE devices.forEach { device -> if (clientEnabledNotifications(device, it)) { gattServer!!.notifyCharacteristicChanged(device, it, confirm) } } } } } }
Finally, check that each connected device has enabled notifications before sending them. We first check if there is a Client Configuration Descriptor. If there isn’t one, we consider the Characteristic correctly set up and notify the device. Otherwise, we must check that we have a saved configuration for this device’s address. If it is not found or not enabled, we will not notify the device.
private fun clientEnabledNotifications(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic): Boolean { val descriptorList = characteristic.descriptors val descriptor = descriptorList.find { isClientConfigurationDescriptor(descriptorList) } ?: // There is no client configuration descriptor, treat as true return true val deviceAddress = device.address val clientConfiguration = clientConfigurations[deviceAddress] ?: // Descriptor has not been set return false return Arrays.equals(clientConfiguration, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) } private fun isClientConfigurationDescriptor(descriptor: BluetoothGattDescriptor?) = descriptor?.let { it.uuid.toString().substring(4, 8) == CLIENT_CONFIGURATION_DESCRIPTOR_ID } ?: false
The timestamp should now be ready to read from the Characteristic! At the end of part 2, the Client should already be logging all characteristic changes in onCharacteristicChanged
.
I hope this blog series was able to demystify some of the Bluetooth Low Energy usage on Android. In this post, we added to our Server a Characteristic with a Client Configuration Descriptor that controlled its notifications. We were able to register for notifications from the Client and receive a timestamp using that Chraracteristic. For the sake of this example, our usage was relatively simple. The Server could save the configurations across connections to allow Clients a quicker setup or use the Indications. Each bluetooth server’s usage of the Client Characteristic Configuration Descriptor can be different, and it will be important to read the defined specifications to understand the handshake. As always, the full source is on a branch in the GitHub repo. Happy coding!
In (https://nerdranchighq.wpengine.com/blog/bluetooth-low-energy-part-1/) we set up our BLE Server and Client and were able to connect them. Now it's time to take a look...
There are many resources available on Bluetooth on Android, but unfortunately many are incomplete snippets, use out-of-date concepts, or only explain half of the...
`MapView`! You can see a `GoogleMap` inside of it! Find your location! Pan! Zoom! Unzoom! There are too many amazing features to talk about....