Search

Bluetooth Low Energy on Android, Part 3

Andrew Lunsford

5 min read

May 5, 2020

Bluetooth Low Energy on Android, Part 3

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.

What are Descriptors?

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.

Add New Characteristic

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

Find Characteristic & Setup Client Configuration Descriptor

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.

Notify Connected Devices

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.

Closing Thoughts

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!

Andrew Lunsford

Author Big Nerd Ranch

Andrew Lunsford is a Solutions Architect who has been at BNR for 10 years. His hobbies include working on his cars, smart home automation, climbing, and traveling.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News