share

ENGINEERING

8min read

How to code Bluetooth Low Energy (BLE) devices: Emberlight case

The key ingredient for the Emberlight project was to integrate Bluetooth Low Energy (BLE) technology into the equation. BLE acts as the main communication agent between the Android device and the Emberlight device. In the following post I am going to present you, a step at a time, how to communicate with BLE devices and give some useful tips for you to start using this amazing piece of tech and connecting BLE gear with Android devices.

1. Finding a device

We first need to find what device we want to connect with BLE. Nowadays, a lot of gear can be found within Bluetooth range, so we have to decide which device we want to connect to. Bluetooth Low Energy devices broadcast advertisements containing such information like MAC address or UUID. This information is necessary to establish the connection between components. In order to start scanning devices, we call:

getBluetoothAdapter().startLeScan(new UUID[]{UUID.fromString(SERVICE_UUID)}, this);

First argument is an array of UUIDs we want to listen to. It means that devices with different UUID will be filtered out while Bluetooth Low Energy scanning. Second is an instance of BluetoothAdapter.LeScanCallback interface, containing one method:

@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
}

BluetoothDevice contains the MAC address of the device, rssi is the strength of the signal, scanRecord is an additional piece of data sent in the advertisement. The most valuable piece of information is saved in scanRecord. So let’s have a closer look at what data we unveiled there:

From Bluetooth 4.0 specification (page 1735):

The format of Advertising data and Scan Response data is shown in Figure 11.1. The data consists of a significant part and a non-significant part. The significant part contains a sequence of AD structures. Each AD structure shall have a Length field of one octet, which contains the Length value, and a Data field of Length octets. The first octet of the Data field contains the AD type field. The content of the remaining Length – 1 octet in the Data field depends on the value of the AD type field and is called the AD data. The non-significant part extends the Advertising and Scan Response data to 31 octets and shall contain all-zero octets.

Now that we have gotten the Advertisement Data structures which contain length, AD type and data, let’s have a look at what the specified AD types exactly mean. Some AD types are described on the next page of the aforementioned documentation. For example 0x01 indicates flags, 0x08 means shortened local name, etc. In Emberlight project the most interesting type is 0xFF - or the Manufacturer specific data. A structure with this AD type contains all additional information given by the manufacturer of the device. Therefore we should use the following method to extract Emberlight specific data:

// -1 is signed value of byte 0xFF
private static final int MANUFACTURER_DATA = -1;

private static final int MANUFACTURER_DATA_LENGTH = 15;

public byte[] getManufacturerAdvertisementData(final byte[] advertisementData) {

    int dataOffset = 2;
    int currentPtr = 0;
    byte[] manufacturerData = null;

    while (currentPtr < advertisementData.length) {
        int dataLength = advertisementData[currentPtr] - 1;

        if (dataLength < 0) {
            break;
        }

        int dataType = advertisementData[currentPtr + 1];

        if (dataType == MANUFACTURER_DATA && dataLength == MANUFACTURER_DATA_LENGTH) {
            manufacturerData = new byte[dataLength];
            for (int i = 0; i < dataLength; i++) {
                manufacturerData[i] = advertisementData[currentPtr + dataOffset + i];
            }
            break;
        }

        currentPtr += dataLength + 2;
    }

    return manufacturerData;
}

So, we iterate through the advertisement until we get all the desired manufacturer data. Then we interpret the received data in accordance with the device specifications:

@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {

    byte[] manufacturerData = bleUtils.getManufacturerAdvertisementData(scanRecord);
    ManufacturerAdvertisement advertisement = new ManufacturerAdvertisement(manufacturerData);

    // Proceed with manufacturer specific advertisment processing
    // Here is where Emberlight-device specific magic happens
}

Scanning can be stopped by:

    public void stopScanning() {
       try {
           getBluetoothAdapter().stopLeScan(this);
           Log.d(TAG, "Scanning stopped");
       } catch (NullPointerException exception) {
           Log.e(TAG, "Can't stop scan. Unexpected NullPointerException", exception);
       }
    }

This is necessary because of Android bug

We encountered problems when we tried to stop scanning on Samsung Galaxy Note 3 with Android 5.0. Sometimes stopLeScan(this) method threw NullPointerException and the only workaround was what you can see above. All other Android devices didn’t cause such problems.

2. Connecting to the device

Now, when we have discovered the MAC address, we connect the device by using:

public boolean connect(String address) {
    Log.i(TAG, "Connecting to " + address);
    if (mBluetoothAdapter == null || address == null) {
        Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
        return false;
    }

    BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(macAddress);

    // We want to directly connect to the device, so we are setting the autoConnect
    // parameter to false.
    BluetoothGatt bluetoothGatt = device.connectGatt(this, false, mGattCallback);
    Log.d(TAG, "Trying to create a new connection.");
    bluetoothGatts.put(address, bluetoothGatt);
    return true;
}

Although connectGatt method returns BluetoothGatt object immediately, the connection is not ready yet. To get the notifications about connection status changed, we need to pass a BluetoothGattCallback as a third argument of the method. When the connection is established, the following method is called:

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    String address = gatt.getDevice().getAddress();

    if (newState == BluetoothProfile.STATE_CONNECTED) {
        Log.i(TAG, "Attempting to start service discovery:" + gatt.discoverServices());

    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
        onDisconnected(gatt);
        Log.i(TAG, "Disconnected from GATT server.");
    }
}

We have to remember that it is possible to receive callback with newState == BluetoothProfile.STATE_DISCONNECTED. It means that the connection failed and that it is necessary to connect again if we want to communicate with the device.

3. Discovering services

When the connection is enabled , it is time to discover what services are available on the device.That is done by calling:

gatt.discoverServices()

As previously, the call is asynchronous and when the discovering is completed,the following method needs to be called:

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        // success, we can communicate with the device
    } else {
        // failure
    }
}

Discovering services process can be quite lengthy, even a few seconds. It depends on how many services were defined in the device and if it’s a known device (which was connected earlier to the gear). If the first scan lasts long, the next ones should be faster.

4. Reading / writing to the device

The next step is reading / writing to the device. Communicating with Bluetooth LE device is performed using so called characteristics. Some characteristics are read-only, others write-only, some are both readable and writable. Characteristics are grouped into services. If we want to find a proper characteristic, we have to iterate through the services provided by the device, then iterate through characteristics in service:

public BluetoothGattCharacteristic findCharacteristic(String macAddress, UUID characteristicUUID) {
    BluetoothGatt bluetoothGatt = bluetoothGatts.get(macAddress);

    if (bluetoothGatt == null) {
        return null;
    }

    for (BluetoothGattService service : bluetoothGatt.getServices()) {

        BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
        if (characteristic != null) {
            return characteristic;
        }
    }

    return null;
}

Now that we have found the characteristic, we can read it:

    protected boolean readCharacteristic(String address, BluetoothGattCharacteristic characteristic) {
       BluetoothGatt bluetoothGatt = bluetoothGatts.get(address);

       if (bluetoothGatt != null) {
           return bluetoothGatt.readCharacteristic(characteristic);

       }
       return false;
    }

Or write:

protected boolean writeCharacteristic(String address, BluetoothGattCharacteristic characteristic) {
    BluetoothGatt bluetoothGatt = bluetoothGatts.get(address);

    if (bluetoothGatt != null) {
        return bluetoothGatt.writeCharacteristic(characteristic);

    }
    return false;
}

Important! You have to set value on the characteristic, before writing:

byte[] value = // value to write;
characteristic.setValue(value);

Otherwise you’ll get DeadObjectException which is very unclear and very hard to understand.

It’s worth noticing that there is no timeout when reading or writing. You have to gage on your own if the length of the operation is too long. For me, when the process took more than 5 seconds it meant that something went wrong. If so, you should disconnect the device and try again.

Another tip. Remember to wait for:

BluetoothGattCallback.onCharacteristicRead(BluetoothGatt gatt,
                                BluetoothGattCharacteristic characteristic,
                                int status)
                                ```
or:
BluetoothGattCallback.onCharacteristicWrite(BluetoothGatt gatt,
                                BluetoothGattCharacteristic characteristic,
                                int status)

Remember that you should callback, before you start another read / write, otherwise all operations will fail and you won’t get any response at all. No matter if you read data from another device or not, you can process only one read / write operation at a time.

5. Closing connection

When the connection is no longer needed, you should close the connection. It is done by calling:

public void disconnect(String address) {

    if (mBluetoothAdapter == null) {
        Log.w(TAG, "BluetoothAdapter not initialized");
    }

    BluetoothGatt bluetoothGatt = bluetoothGatts.get(address);

    if (bluetoothGatt != null) {
        bluetoothGatt.disconnect();
        bluetoothGatts.remove(address);
    }
}

Then we have to wait for the callback onConnectionStateChange(BluetoothGatt gatt, int status, int newState) with newState == BluetoothProfile.STATE_DISCONNECTED and then close the gatt by calling:

bluetoothGatt.close();

It’s important to close gatt, because the connection pool is limited on Android. If you reach the maximum, no new connection can be made until you restart your phone (even restarting Bluetooth will not help).

Also, it’s advisable not to reuse BluetoothGatt after disconnecting, as it can cause some connectivity issues.

Conclusion

Although communication with BLE device seems to be easy at first sight, there are some points that can confuse developers. Processing only one read/write operation at a time, not reusing BluetoothGatt after disconnecting are the most important but implicit assumptions you should keep in mind. Despite this, grasping knowledge of BLE implementation is really valuable because it opens up new possibilities to create great products such as smart bulbs, armbands, home automation systems, etc. It is necessary to know how to manipulate those devices and BLE is a great way of doing it. In addition to this, our Android Dev team has released a library on github to ease Android BLE development.

share


WojtekSenior Software Engineer