Save 37% off PRO during our Black Friday Sale! »

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.

2307a37297162f815342545a2068b2f1?s=128

Erik Hellman

October 21, 2021
Tweet

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. @ErikHellman - DroidConBerlin 2021 MAD at Bluetooth

  3. None
  4. Standards are hard

  5. GATT GAP PHY Peripheral Central Advertising Profile Characteristic Service Descriptor

  6. Bluetooth Low Energy - Basics

  7. Generic Access Profile - GAP

  8. Broadcasting/Advertising

  9. Generic Attribute Profile - GATT

  10. Client/Server Communications

  11. 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
  12. Services • Identi fi ed by a UUID • Must

    be discovered after each successful connection - every time! • Contains a collection of Characteristics
  13. 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!
  14. Descriptors • Identi fi ed with a UUID • Information

    about a characteristics (name, type, etc.) • Enable/Disable noti fi cations
  15. Bluetoth SIG Base UUID xxxxxxxx-0000-1000-8000-00805F9B34FB

  16. Bluetooth 16-bit UUIDs

  17. Using 16-bit UUIDs Media Player Name Characteristic UUID:
 00002B93-0000-1000-8000-00805F9B34FB

  18. Most custom devices use custom UUIDs!

  19. Android and Bluetooth LE

  20. Android versions and Bluetooth Low Energy •API level 16 -

    😭 •API level 21 - 😟 •API level 26 - 🙂 •API level 31 - 😃
  21. Peripheral Discovery & Pairing

  22. None
  23. None
  24. Pairing a device with CompanionDeviceManager val request = AssociationRequest.Builder() .addDeviceFilter(

    BluetoothLeDeviceFilter.Builder() .setNamePattern(NAME_PATTERN) .build() ) .setSingleDevice(true) .build()
  25. 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 )
  26. None
  27. Communication

  28. 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 )
  29. 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()
  30. 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)) } }
  31. 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… }
  32. 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)
  33. 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) } }
  34. 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) }
  35. 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 }
  36. 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)
  37. PHY Options (Bluetooth 5.x) 100 meters (line of sight)

  38. PHY Options (Bluetooth 5.x) 10011010

  39. PHY Options (Bluetooth 5.x) Symbols 10011010 Symbols (frequency shifts)

  40. PHY Options (Bluetooth 5.x) More symbols - better range, lower

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

    throughput 10011010 Symbols (frequency shifts)
  42. 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
  43. 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 )
  44. None
  45. Key take-aways • API level 26 • CompanionDeviceManager • Characteristics

    should never be read AND write! • Only one GATT operation at a time!
  46. 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
  47. Thank you for listening! @ErikHellman