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 full-size slide

  2. @ErikHellman - DroidConBerlin 2021
    MAD at Bluetooth

    View full-size slide

  3. Standards are hard

    View full-size slide

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

    View full-size slide

  5. Bluetooth Low Energy - Basics

    View full-size slide

  6. Generic Access Profile - GAP

    View full-size slide

  7. Broadcasting/Advertising

    View full-size slide

  8. Generic Attribute Profile - GATT

    View full-size slide

  9. Client/Server Communications

    View full-size slide

  10. 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 full-size slide

  11. Services
    • Identi
    fi
    ed by a UUID

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

    • Contains a collection of Characteristics

    View full-size slide

  12. 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 full-size slide

  13. Descriptors
    • Identi
    fi
    ed with a UUID

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

    • Enable/Disable noti
    fi
    cations

    View full-size slide

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

    View full-size slide

  15. Bluetooth 16-bit UUIDs

    View full-size slide

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

    00002B93-0000-1000-8000-00805F9B34FB

    View full-size slide

  17. Most custom devices use
    custom UUIDs!

    View full-size slide

  18. Android and Bluetooth LE

    View full-size slide

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

    •API level 21 - 😟

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

    View full-size slide

  20. Peripheral Discovery & Pairing

    View full-size slide

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

    .addDeviceFilter(

    BluetoothLeDeviceFilter.Builder()

    .setNamePattern(NAME_PATTERN)

    .build()

    )

    .setSingleDevice(true)

    .build()

    View full-size slide

  22. 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 full-size slide

  23. Communication

    View full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

  27. 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 full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

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

    View full-size slide

  34. PHY Options (Bluetooth 5.x)
    10011010

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. Key take-aways
    • API level 26

    • CompanionDeviceManager

    • Characteristics should never be read AND write!

    • Only one GATT operation at a time!

    View full-size slide

  41. 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 full-size slide

  42. Thank you for listening!
    @ErikHellman

    View full-size slide