Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Bluetooth Low Energy for Modern Android Development

Bluetooth Low Energy for Modern Android Development

Bluetooth Low Energy has had a rough time on Android. While the API was introduced back in Jelly Beans, it has suffered numerous bugs and flaws over the years.

Fortunately, things have improved since then. BLE is now easier to work with, much thanks to small additions to the platform APIs, but also with the introduction of new services and APIs in the framework.

In this session we will look at how to write apps for BLE peripherals like smart watches and other IoT devices. We'll look at best practices, common pitfalls, and even what to require from the engineers writing the embedded code for peripherals.

Hopefully this session will be useful for all who are working with BLE in any capacity, or simply have an interest to learn more.

Erik Hellman

October 21, 2021
Tweet

More Decks by Erik Hellman

Other Decks in Programming

Transcript

  1. @ErikHellman - DroidConBerlin 2021 Bluetooth Low Energy for Modern Android

    Development Or how to not go MAD when doing BLE… Erik Hellman, Head of Development - Iteam Solutions
  2. GATT - Generic Attribute Profile Pro fi le Service Characteristic

    Characteristic Descriptor Descriptor Descriptor Descriptor Service Characteristic Descriptor Descriptor Only a logical grouping Doesn’t exist in real life! The actual communication point Information and con fi guration of a characteristics
  3. Services • Identi fi ed by a UUID • Must

    be discovered after each successful connection - every time! • Contains a collection of Characteristics
  4. Characteristics • Identi fi ed with a UUID • Stores

    a value (byte array) • Default maximum size of 21 bytes • Contains a collection of Descriptors • Can not handle concurrent read & writes - use two separate characteristics!
  5. Descriptors • Identi fi ed with a UUID • Information

    about a characteristics (name, type, etc.) • Enable/Disable noti fi cations
  6. Android versions and Bluetooth Low Energy •API level 16 -

    😭 •API level 21 - 😟 •API level 26 - 🙂 •API level 31 - 😃
  7. Pairing a device with CompanionDeviceManager val request = AssociationRequest.Builder() .addDeviceFilter(

    BluetoothLeDeviceFilter.Builder() .setNamePattern(NAME_PATTERN) .build() ) .setSingleDevice(true) .build()
  8. Pairing a device with CompanionDeviceManager companionDeviceManager.associate(request, object : CompanionDeviceManager.Callback() {

    override fun onDeviceFound(sender: IntentSender?) { activity.startIntentSenderForResult( sender, SELECT_DEVICE_REQUEST_CODE, null, 0, 0, 0 ) } override fun onFailure(error: CharSequence?) { // Handle the failure ... } }, null )
  9. GATT events val manager = context.getSystemService<BluetoothManager>() val device = manager

    ?. adapter ?. getRemoteDevice(address) val gatt = device ?. connectGatt( context, true, // Try to connect until we succeed gattCallback, // Callbacks BluetoothDevice.TRANSPORT_LE, // Use BLE, not classic BluetoothDevice.PHY_OPTION_NO_PREFERRED // Only works if autoConnect is false )
  10. GATT events sealed class GattEvent data class CharacteristicChanged( val characteristic:

    BluetoothGattCharacteristic ) : GattEvent() data class CharacteristicRead( val characteristic: BluetoothGattCharacteristic, val status: Int ) : GattEvent() data class CharacteristicWritten( val characteristic: BluetoothGattCharacteristic, val status: Int ) : GattEvent()
  11. BluetoothGattCallback class GattCallbackEvents : BluetoothGattCallback() { private val _events =

    MutableSharedFlow<GattEvent>(extraBufferCapacity = 10) val events: SharedFlow<GattEvent> = _events override fun onConnectionStateChange( gatt: BluetoothGatt?, status: Int, newState: Int ) { _events.tryEmit(ConnectionStateChanged(status, newState)) } override fun onCharacteristicChanged( gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic? ) { if (characteristic ! = null) { _events.tryEmit(CharacteristicChanged(characteristic)) } }
  12. GattDevice - Our BLE API facade class GattDevice(callbacks: GattCallbackEvents) {

    private val events = callbacks.events 
 private var bluetoothGatt: BluetoothGatt? = null private fun requireGatt(): BluetoothGatt = bluetoothGatt ?: throw IllegalStateException("BluetoothGatt is null") …wrapper code goes here… }
  13. Call & wait suspend fun writeCharacteristic( characteristic: BluetoothGattCharacteristic ): CharacteristicWritten

    = events .onSubscription { requireGatt().writeCharacteristic(characteristic) } .firstOrNull { it is CharacteristicRead & & it.characteristic.uuid == characteristic.uuid } as CharacteristicWritten? ?: CharacteristicWritten(characteristic, BluetoothGatt.GATT_FAILURE)
  14. Only one ongoing call at a time! private suspend fun

    <T> Mutex.queueWithTimeout( timeout: Long = DEFAULT_GATT_TIMEOUT, block: suspend CoroutineScope.() - > T ): T { withLock { withTimeout(timeMillis = timeout, block = block) } }
  15. Only one ongoing call at a time! private val mutex

    = Mutex() 
 
 suspend fun writeCharacteristic( characteristic: BluetoothGattCharacteristic ): CharacteristicWritten = mutex.queueWithTimeout { events .onSubscription { requireGatt().writeCharacteristic(characteristic)) } .firstOrNull { it is CharacteristicRead && it.characteristic.uuid == characteristic.uuid } as CharacteristicWritten? ? : CharacteristicWritten(characteristic, BluetoothGatt.GATT_FAILURE) }
  16. Enabling notifications companion object { private val CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

    } suspend fun registerNotifications(characteristic: BluetoothGattCharacteristic): Boolean = mutex.queueWithTimeout { val result = characteristic.descriptors.find { it.uuid == CCCD } ?. let { descriptor - > descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE events .onSubscription { requireGatt().setCharacteristicNotification(characteristic, true) requireGatt().writeDescriptor(descriptor) } .firstOrNull { it is DescriptorWritten && it.descriptor.characteristic.uuid == descriptor.characteristic.uuid && it.descriptor.uuid == descriptor.uuid } as DescriptorWritten? ?: DescriptorWritten(descriptor, BluetoothGatt.GATT_FAILURE) } result ?. status = = BluetoothGatt.GATT_SUCCESS }
  17. Enabling notifications // Tell the Android OS to notify our

    app for this characteristic requireGatt().setCharacteristicNotification(characteristic, true) // Tell the peripheral to notify our phone for this characteristic requireGatt().writeDescriptor(descriptor)
  18. PHY Options (Bluetooth 5.x) More symbols - better range, lower

    throughput 10011010 Symbols (frequency shifts)
  19. PHY Options (Bluetooth 5.x) Less symbols - lower range, better

    throughput 10011010 Symbols (frequency shifts)
  20. PHY Options (Bluetooth 5.x) 1 MBit/s, 100 meter range LE

    1M PHY 2 MBit/s, 80 meter range LE 2M PHY 125 Kbit/s, 400 meter range LE Coded PHY
  21. PHY Options (Bluetooth 5.x) BluetoothDevice.PHY_LE_1M_MASK // Default range and throughput

    BluetoothDevice.PHY_LE_2M_MASK // Lower range and higher throughput BluetoothDevice.PHY_LE_CODED_MASK // Longer range and lower throughput BluetoothDevice.PHY_OPTION_S2 // S=2 Coding for LE Coded PHY (default for LE Coded) BluetoothDevice.PHY_OPTION_S8 // S=8 Coding for LE Coded PHY (longest possible range) // Best possible range, but lowest throughput bluetoothGatt.setPreferredPhy( BluetoothDevice.PHY_LE_CODED_MASK, BluetoothDevice.PHY_LE_CODED_MASK, BluetoothDevice.PHY_OPTION_S8 )
  22. Key take-aways • API level 26 • CompanionDeviceManager • Characteristics

    should never be read AND write! • Only one GATT operation at a time!
  23. References • Bluetooth PHY – How it Works and How

    to Leverage it
 https://punchthrough.com/crash-course-in-2m-bluetooth-low-energy-phy/ • Exploring Bluetooth 5 – Going the Distance
 https://www.bluetooth.com/blog/exploring-bluetooth-5-going-the-distance/ • Modern Android Bluetooth LE demo project
 https://github.com/ErikHellman/ModernAndroidBluetoothLE