Bluetooth Low Energy (BT LE) is a key technology for the IoT world. It allows smart devices such as smartphones, tablets or wearables to communicate with smart electronic environment such as smartwatches, smart beds, sensors etc. Bluetooth LE in Android is available from version 4.3. It allows for significant battery saving on device and constant connectivity to several devices at the same time. Yet, the implementation of BT LE is not as simple as it may seem and has several pitfalls. Below I'll show how to implement it the right way and make the connection as stable as possible.

Prior to talking about Bluetooth LE connection, let's talk a little bit about GATT application, or GATT profile.

What is GATT?

The Generic Attributes (GATT) define a hierarchical data structure that is exposed to connected Bluetooth LE devices.

GATT profiles enable extensive innovation while still maintaining full interoperability with other Bluetooth® devices. The profile describes a use case, roles and general behaviors based on the GATT functionality. Services are collections of characteristics and relationships to other services that encapsulate the behavior of part of a device. This also includes hierarchy of services, characteristics and attributes used in the attribute server.

How to use GATT on Android:

  1. Connect GATT
  2. Discover services
  3. Enable notifications
  4. Start communicating

Device scan

Devices can be scanned either with or without any filters and settings. If you know a required device name or UUID, you can significantly save the energy by applying filter. UUID should be in a 16-bit format.

What is 16-bit UUID?

Regular UUID: 83F94223-98DC-2387-1212-FD34158790E

Bluetooth SIG base UUID: XXXXXXXX-0000-1000-8000-00805F9B34FB

32 bits left, top 16 are always 0

0x180D (Heart Rate Service) is the same as

0000180D-0000-1000-8000-00805F9B34FB

Tip: Don’t perform device scan and connection simultaneously, as a scanning process can interfere the connection. Skip discovery if possible - If you can assume the device is present - try to connect it.

Scanning on Android API >= v21+:

ScanSettings scanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();

//If name or address of peripheral is known
ScanFilter scanFilter = new ScanFilter.Builder()
.setDeviceName(deviceName)
.setDeviceAddress(deviceAddress)
.build();

BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothLeScanner bluetoothLeScanner = adapter.getBluetoothLeScanner();
bluetoothLeScanner.startScan(Collections.singletonList(scanFilter), scanSettings, new MyScanCallback());

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 (showldConnect(result)) {
BluetoothDevice device = result.getDevice();
scaner.stopScan(this);
device.connectGatt(context, false, gattCallback);
}
}
}

       

Scanning on Android API < v21:

//Adding UUID’s filters array is not recommended
BluetoothAdapter adapter = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
adapter.startLeScan(new MyLeScanCallback);

Connecting to Gatt

Tip: Don’t use Auto-connect for Gatt connection. Auto-connect is flaky and doesn’t behave as expected.

private void connectToPeripheral(BluetoothDevice device) {
MyGattCallback gattCallback = new MyGattCallback();
//Always use false on auto-connect parameter
device.connectGatt(context, false, gattCallback);
}

GattCallback implementation

Tip: All BLE callbacks happen on binder thread. Never block the BluetoothGattCallback methods. Use HandlerThread for all GATT operations.

//Create and start HandlerThread to handle GattCallbacks
public MyGattCallback() {
HandlerThread handlerThread = new HandlerThread("BLE-Worker");
handlerThread.start();
bleHandler = new Handler(handlerThread.getLooper(), this);
}

//Don’t forget to dispose it after finish work
public void dispose() {
bleHandler.removeCallbacksAndMessages(null);
bleHandler.getLooper().quit();
}

 

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
//Discover services when Gatt is connected
bleHandler.obtainMessage(MSG_DISCOVER_SERVICES, gatt).sendToTarget();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
bleHandler.obtainMessage(MSG_GATT_DISCONNECTED, gatt).sendToTarget();
} else {
//If received any error
reconnectOnError("onConnectionStateChange", status);
}
}

Tip: Services must be discovered! discoverServices() must be called before communicating


  @Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_DISCOVER_SERVICES:
BluetoothGatt gatt = ((BluetoothGatt) msg.obj);
gatt.discoverServices();
return true;
case MSG_SERVICES_DISCOVERED:
BluetoothGatt gatt = ((BluetoothGatt) msg.obj);
subscribeNotifications(gatt);
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
return true;
case MSG_DATA_READ:
processDataReceived((byte[]) msg.obj);
return true;
case MSG_RECONNECT:
broadcastUpdate(ACTION_RECONNECT_DEVICE, deviceName);
return true;
case MSG_GATT_DISCONNECTED:
broadcastUpdate(ACTION_GATT_DISCONNECTED);
return true;
}
}

Getting ready to read and write

Tip: Most error codes are undocumented. Treat everything but GATT_SUCCESS as an error. When an error occurs - disconnect, close and reconnect. Most BLE errors are “unrecoverable”!

 

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
bleHandler.obtainMessage(MSG_SERVICES_DISCOVERED, gatt).sendToTarget();
} else {
reconnectOnError("onServicesDiscovered", status);
}
}

public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_SERVICES_DISCOVERED:
BluetoothGatt gatt = ((BluetoothGatt) msg.obj);
commService = gatt.getService(DATA_SERVICE_ID);
inputChar = commService.getCharacteristic(INPUT_CHAR_ID);
outputChar = commService.getCharacteristic(OUTPUT_CHAR_ID);
return true;

Send data and receive response

Tip: 20 byte write limit. The max size of a write operation is 20 bytes. (Except devices supporting BluetoothGatt.requestMtu())

 

//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 characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS && c.getUUID().equals(outputChar.getUUID())) {
//Write operation successful - process with next chunk!
}
}

public void readData(BluetoothGatt gatt, BluetoothGattCharacteristic c) {
gatt.readCharacteristic(c);
}

@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS &&
getReadCharacteristicUuid().equals(characteristic.getUuid())) {
byte[] data = characteristic.getValue();
if (data == null) {
Log.e(TAG, "onCharacteristicRead: data = null");
return;
}
bleHandler.obtainMessage(MSG_DATA_READ, data).sendToTarget();
} else {
reconnectOnError("onCharacteristicRead", status);
}
}


Subscribe for data change notifications

Tip: If possible, enable notifications for data changes - it’s more energy efficient and allows to get update as soon as it appears.

 

private void subscribeNotifications(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_NOTIICATION_VALUE
desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(desc);
}

Close connection

close () == “Unregister application”

disconnect() = cut connection

When in doubt, do both:

gatt.disconnect();

gatt.close();

When debugging - use the following rules

  1. Don’t put breakpoints in your BluetoothGattCallbacks
  2. Log all BluetoothGAttCallback calls and their data
  3. Enable Bluetooth HCI snoop log (at OS developer options)

Summary

So let’s summarize the key moments that make your connection stable and fast. Once you've found a needed device while scanning - stop scan and only then connect to it. Try to connect the device with a known address instead of rescanning it. Do all work out of BluetoothGattCallback callback methods - use HadlerThread for that purpose. Reconnect to the device if any error code came to the GATT callback method. And don’t forget to close the connection when you finished or reconnecting - calling gatt.disconnect() and gatt.close() is ok in most cases.

Are you looking to hire seasoned Android developers for your in-house / offshore project fast and with no IT, HR and admin hassle?
Send us your request!

 

Yaroslav is our PMO. He has 8+ years of experience working in IT and has made his way from a purchasing and store manager to a famous Agile evangelist and coach in Odessa, Ukraine. Yaroslav is the author of the "I am PM" simulator that aims to help Agile teams become multi-tasking, self-organizing and more efficient by regularly boosting and upgrading their PM and Agile skills. Feel free to contact Yaroslav on LinkedIn to get in touch!

Leave a comment

Get a Quote