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

    View Slide

  2. @ErikHellman - DroidConBerlin 2021
    MAD at Bluetooth

    View Slide

  3. View Slide

  4. Standards are hard

    View Slide

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

    View Slide

  6. Bluetooth Low Energy - Basics

    View Slide

  7. Generic Access Profile - GAP

    View Slide

  8. Broadcasting/Advertising

    View Slide

  9. Generic Attribute Profile - GATT

    View Slide

  10. Client/Server Communications

    View Slide

  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

    View Slide

  12. Services
    • Identi
    fi
    ed by a UUID

    • Must be discovered after each successful connection - every time!

    • Contains a collection of Characteristics

    View Slide

  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!

    View Slide

  14. Descriptors
    • Identi
    fi
    ed with a UUID

    • Information about a characteristics (name, type, etc.)

    • Enable/Disable noti
    fi
    cations

    View Slide

  15. Bluetoth SIG Base UUID
    xxxxxxxx-0000-1000-8000-00805F9B34FB

    View Slide

  16. Bluetooth 16-bit UUIDs

    View Slide

  17. Using 16-bit UUIDs
    Media Player Name Characteristic UUID:

    00002B93-0000-1000-8000-00805F9B34FB

    View Slide

  18. Most custom devices use
    custom UUIDs!

    View Slide

  19. Android and Bluetooth LE

    View Slide

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

    •API level 21 - 😟

    •API level 26 - 🙂
    •API level 31 - 😃

    View Slide

  21. Peripheral Discovery & Pairing

    View Slide

  22. View Slide

  23. View Slide

  24. Pairing a device with CompanionDeviceManager
    val request = AssociationRequest.Builder()

    .addDeviceFilter(

    BluetoothLeDeviceFilter.Builder()

    .setNamePattern(NAME_PATTERN)

    .build()

    )

    .setSingleDevice(true)

    .build()

    View Slide

  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

    )

    View Slide

  26. View Slide

  27. Communication

    View Slide

  28. GATT events
    val manager = context.getSystemService()

    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

    )

    View Slide

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

    View Slide

  30. BluetoothGattCallback
    class GattCallbackEvents : BluetoothGattCallback() {

    private val _events = MutableSharedFlow(extraBufferCapacity = 10)

    val events: SharedFlow = _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))

    }

    }

    View Slide

  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…

    }

    View Slide

  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)

    View Slide

  33. Only one ongoing call at a time!
    private suspend fun Mutex.queueWithTimeout(

    timeout: Long = DEFAULT_GATT_TIMEOUT,

    block: suspend CoroutineScope.()
    -
    >
    T

    ): T {

    withLock { withTimeout(timeMillis = timeout, block = block) }

    }

    View Slide

  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)

    }

    View Slide

  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

    }

    View Slide

  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)

    View Slide

  37. PHY Options (Bluetooth 5.x)
    100 meters (line of sight)

    View Slide

  38. PHY Options (Bluetooth 5.x)
    10011010

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    )

    View Slide

  44. View Slide

  45. Key take-aways
    • API level 26

    • CompanionDeviceManager

    • Characteristics should never be read AND write!

    • Only one GATT operation at a time!

    View Slide

  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

    View Slide

  47. Thank you for listening!
    @ErikHellman

    View Slide