Practical Bluetooth Low Energy on Android

Practical Bluetooth Low Energy on Android

Bluetooth Low Energy was announced for Android 4.3 (Jelly Bean MR2, API level 18) and the API has received some updates with each new version since then. Unfortunately, the way the API works and the number of undocumented "features" in the Bluetooth stack on Android has made it very difficult to work with. Regardless if you're doing something simple like scanning for beacons or doing two-way communciation with another device, you're bound to run into problems that can be both hard to identify and difficult to fix.

In this session we will go through the most common pitfalls regarding Bluetooth LE on Android and how to deal with them. You'll learn how to properly scan for devices, connect to them, and exchange data. An introduction to Bluetooth LE concepts, such as GATT, services, characteristics and descriptors will also be included. After this session, you'll be better prepared to implement Bluetooth LE in your own applications.

2307a37297162f815342545a2068b2f1?s=128

Erik Hellman

June 17, 2016
Tweet

Transcript

  1. Practical Bluetooth Low Energy on Android Erik Hellman Practical Bluetooth

    Low Energy on Android - @ErikHellman / www.hellso!.se
  2. Harald "Bluetooth" Gormsson Practical Bluetooth Low Energy on Android -

    @ErikHellman / www.hellso!.se
  3. ~ 1000 years later... Practical Bluetooth Low Energy on Android

    - @ErikHellman / www.hellso!.se
  4. 1994 - Bluetooth is invented Practical Bluetooth Low Energy on

    Android - @ErikHellman / www.hellso!.se
  5. Mr Bluetooth, Jim Kardach, Intel Practical Bluetooth Low Energy on

    Android - @ErikHellman / www.hellso!.se
  6. The Logo Practical Bluetooth Low Energy on Android - @ErikHellman

    / www.hellso!.se
  7. Versions Practical Bluetooth Low Energy on Android - @ErikHellman /

    www.hellso!.se
  8. Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se

  9. Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se

  10. Beacon vs. Peripheral Practical Bluetooth Low Energy on Android -

    @ErikHellman / www.hellso!.se
  11. Beacon Advertisements Practical Bluetooth Low Energy on Android - @ErikHellman

    / www.hellso!.se
  12. BLE Advertisement Packets Practical Bluetooth Low Energy on Android -

    @ErikHellman / www.hellso!.se
  13. Advertisement payload · 128-bit Service UUIDs · 16-bit Service Class

    UUID · Shortened Local Name · Complete Local Name Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  14. 16-bit UUID? Regular UUID: 75BEB663-74FC-4871-9737-AD184157450E Bluetooth SIG base UUID: XXXXXXXX-0000-1000-8000-00805F9B34FB

    32 bits le!, top 16 are always 0 0x180D (Heart Rate Service) is the same as 0000180D-0000-1000-8000-00805F9B34FB Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  15. Pro tip 1: Advertisement != Actual The advertisement does not

    have to match the actual services on a peripheral! Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  16. Scanning on Android (API lvl 21+) - Basic private void

    startScanningForDevices(BluetoothAdapter adapter) { bluetoothLeScanner = adapter.getBluetoothLeScanner(); callback = new MyScanCallback(this); bluetoothLeScanner.startScan(callback); } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  17. Scanning on Android - Name or address ScanSettings scanSettings =

    new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build(); // Name or address of peripherla is known ScanFilter.Builder builder = new ScanFilter.Builder(); builder.setDeviceAddress(deviceAddress); // MAC address builder.setDeviceName(deviceName); // Name of device ScanFilter scanFilter = builder.build(); bluetoothLeScanner.startScan(Collections.singletonList(scanFilter), scanSettings, callback); Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  18. Scanning on Android - Service UUID ScanFilter scanFilter = new

    ScanFilter.Builder() .setServiceUuid(SERVICE_UUID) .build(); // Peripheral advertisement contains a service UUID bluetoothLeScanner.startScan(Collections.singletonList(scanFilter), scanSettings, callback); Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  19. Scanning on Android - Service data // Scan for peripherals

    with a certain state in the serviceData ScanFilter scanFilter = new ScanFilter.Builder() .setServiceData(SERVICE_DATA_UUID, serviceData) .build(); bluetoothLeScanner.startScan(Collections.singletonList(scanFilter), scanSettings, new MyLeScanCallback()); Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  20. Scanning on Android - Manufacturer data // Scan for specific

    manufacturer data ScanFilter scanFilter = new ScanFilter.Builder() .setManufacturerData(MFR_ID, mfrData) .build(); bluetoothLeScanner.startScan(Collections.singletonList(scanFilter), scanSettings, new MyLeScanCallback()); Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  21. ScanCallback public class MyScanCallback extends ScanCallback { // fields and

    constructor omitted @Override public void onScanResult(int callbackType, ScanResult result) { // Will execute on the main thread! // Do the work below on a worker thread instead! if(shouldWeConnect(result)) { BluetoothDevice device = result.getDevice(); scanner.stopScan(this); device.connectGatt(context, false, gattCallback); } } } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  22. ScanResult - Manufacturer data private boolean shouldWeConnect(ScanResult result) { ScanRecord

    scanRecord = result.getScanRecord(); byte[] mfrData = scanRecord.getManufacturerSpecificData(MFR_ID); List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids() Map<ParcelUuid, byte[]> serviceData = scanRecord.getServiceDate(); return // some custom filtering...; } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  23. Pro tip 2: Skip discovery if possible! If you can

    assume the device is present: public void connectToDevice(BluetoothAdapter adapter, String address, MyGattCallback gattCallback) { BluetoothDevice device = adapter.getRemoteDevice(address); // TODO Set a timeout somewhere... device.connectGatt(context, false, gattCallback); } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  24. Peripheral Structure Practical Bluetooth Low Energy on Android - @ErikHellman

    / www.hellso!.se
  25. Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se

  26. GATT on Android 1. Connect to GATT 2. Discover services

    3. Enable notifications 4. Start communicating! Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  27. private void connectToPeripheral(BluetoothDevice device) { MyGattCallback gattCallback = new MyGattCallback();

    // Always use false on auto-connect parameter device.connectGatt(context, false, gattCallback); } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  28. Pro tip nr 3: Don't use auto- connect Auto-connect is

    flaky and doesn't behave as expected. Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  29. BluetoothGattCallback public class MyGattCallback extends BluetoothGattCallback { @Override public void

    onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { super.onConnectionStateChange(gatt, status, newState); if(newState == BluetoothProfile.STATE_CONNECTED) { // DON'T CALL THIS HERE!!! gatt.discoverServices(); Log.d(TAG, "Callback on thread: " + Thread.currentThread().getName()); } } } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  30. Pro tip nr 4: BLE and Threads All BLE callbacks

    happen on a Binder thread! Never block the BluetoothGattCallback methods! Use a HandlerThread for all GATT operations! Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  31. Android BLE and Threads 1/3 public class MyGattCallback extends BluetoothGattCallback

    implements Handler.Callback { public static final int MSG_DISCOVER_SERVICES = 10; private Handler bleHandler; public MyGattCallback() { HandlerThread handlerThread = new HandlerThread("BLE-Worker"); handlerThread.start(); bleHandler = new Handler(handlerThread.getLooper(), this); } public void dispose() { // TODO Probably disconnect as well? bleHandler.removeCallbacksAndMessages(null); bleHandler.getLooper().quit(); } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  32. Android BLE and Threads 2/3 @Override public void onConnectionStateChange(BluetoothGatt gatt,

    int status, int newState) { super.onConnectionStateChange(gatt, status, newState); if(newState == BluetoothProfile.STATE_CONNECTED) { bleHandler.obtainMessage(MSG_DISCOVER_SERVICES, gatt).sendToTarget(); } } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  33. Android BLE and Threads 3/3 @Override public boolean handleMessage(Message msg)

    { switch (msg.what) { case MSG_DISCOVER_SERVICES: BluetoothGatt gatt = (BluetoothGatt) msg.obj; gatt.discoverServices(); break; } return true; } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  34. Ready to read and write! @Override public void onServicesDiscovered(BluetoothGatt gatt,

    int status) { super.onServicesDiscovered(gatt, status); bleHandler.obtainMessage(MSG_SERVICES_DISCOVERED, gatt).sendToTarget(); } @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_SERVICES_DISCOVERED: // Discovery successful, fetch services, chars and descs... BluetoothGatt gatt = (BluetoothGatt) msg.obj; commService = gatt.getService(DATA_SERVICE_ID); inputChar = commService.getCharacteristic(INPUT_CHAR_ID); outputChar = commService.getCharacteristic(OUTPUT_CHAR_ID); break; } } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  35. Pro tip 5: Services must be discovered! discoverServices() must be

    called before communicating Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  36. Reading data is asynchronous public void readData(BluetoothGatt gatt, BluetoothGattCharacteristic c)

    { gatt.readCharacteristic(c); } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic c, int status) { if(status == BluetoothGatt.GATT_SUCCESS && INPUT_CHAR_UUID.equals(c.getUuid()) { byte[] value = c.getValue(); bleHandler.obtainMessage(MSG_NOTIFY_DATA_CHANGED, value) .sendToTarget(); } } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  37. Subscribing for data change notifications private void enableDataNotifications(BluetoothGatt gatt BluetoothGattCharacteristic

    c) { gatt.setCharacteristicNotification(c, true); // This is usually needed as well... BluetoothGattDescriptor desc = c.getDescriptor(INPUT_DESC_ID); // Could also be ENABLE_NOTIFICATION_VALUE desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(desc); } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  38. Subscribing for data change notifications @Override public void onCharacteristicChanged(BluetoothGatt gatt,

    BluetoothGattCharacteristic c) { super.onCharacteristicChanged(gatt, c); if(INPUT_CHAR_UUID.equals(c.getUuid()) { byte[] value = c.getValue(); bleHandler.obtainMessage(MSG_NOTIFY_DATA_CHANGED, value) .sendToTarget(); } } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  39. Pro tip 6: If possible, enable notifications Practical Bluetooth Low

    Energy on Android - @ErikHellman / www.hellso!.se
  40. Writing data (max 20 bytes) // Should always be called

    on our BLE thread! private void writeDataToChar(byte[] data) { if(data.length > 20) { throw new IllegalArgumentException(); } outputChar.setValue(data); gatt.writeCharacteristic(characteristic); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic c, int status) { if (status == BluetoothGatt.GATT_SUCCESS && c.getUuid().equals(outputChar.getUuid())) { // Write operation successful, proceed with next chunk! } } Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  41. Pro tip 7: 20 byte write limit The max size

    of a write operation is 20 bytes! (except for devices supporting BluetoothGatt.requestMtu()) Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  42. close() vs disconnect() close() == "unregister app" disconnect() == cut

    connection When in doubt, do both. gatt.disconnect(); gatt.close(); Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  43. Errors! Sony Mobile owners will see this... 03-06 13:00:11.994: D/BluetoothGatt(26771):

    registerApp() 03-06 13:00:11.994: D/BluetoothGatt(26771): registerApp() - UUID='...' 03-06 13:00:12.004: D/BluetoothGatt(26771): onClientRegistered() - status=0 clientIf=5 03-06 13:00:42.004: D/BluetoothGatt(26771): onClientConnectionState() - status=133 clientIf=5 device='...' Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  44. Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se

  45. Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se

  46. Pro tip 8: Most error codes are undocumented! Treat everything

    but GATT_SUCCESS as error Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  47. Pro tip 9: When an error occurs; disconnect, close and

    reconnect. Most BLE errors are "unrecoverable" Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  48. Debugging · Don't put breakpoints in your BluetoothGattCallback callbacks ·

    Log all BluetoothGattCallback calls and their data · Enable Bluetooth HCI snoop log Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se
  49. Practical Bluetooth Low Energy on Android - @ErikHellman / www.hellso!.se

  50. Thank you for listening! Practical Bluetooth Low Energy on Android

    - @ErikHellman / www.hellso!.se