Creating a Bluetooth Low Energy Central Implementation on Android

-

At Luminis Arnhem we are currently working together with Nedap on the MACE project, a prototype implementation of a site access control system. In the MACE scenario, the users can access a physical location/open a door using a digital identity card on their mobile device. These digital identities replace those cumbersome physical cards that either take up space in your wallet or always seem to disappear when you need them.

The MACE mobile application receives these identities from a server application, which are then sent to a MACE reader device via Bluetooth Low Energy (BLE), Near Field Communication (NFC) or can be read by the reader via a Quick Response (QR) code. The idea is that if the correct identity is read by the reader, i.e. an identity that has the authority to gain access to the location guarded by the reader, then the user gains access to a physical location.

In order to create an initial prototype we needed to delve into Android BLE. In this blog I will describe how BLE works in the MACE scenario as well as how the implementation looks like in Android.

For more information on the MACE project itself, visit the MACE homepage.

On BLE 

There are two roles in BLE communication. These are the central and peripheral roles. The central role is responsible for scanning for advertisements which are made by peripherals. Once the central and peripheral are in range of each other, the central will start to receive these advertisements and can choose to connect to a peripheral.

Think of it as the central being a job seeker and a peripheral being one of those recruiters on LinkedIn. What I’m trying to say about peripherals is that they continuously (in most cases) advertise without even knowing if the central has any interest or use for its services. Luckily, the central can use a filter to only see advertisements that it finds interesting. This is done based on a UUID filter, which on the application level only shows advertisements that contain a service with that UUID.

Each advertisement contains (at least) the following information:

Bluetooth Device Address: Similar to a MAC address used to identify a BLE device.

Device name: A custom name for the peripheral device which can be configured.

RSSI value: The Received Signal Strength Indicator in decibel per meter. This is a value that indicates the strength of the signal, ranging from -100 to 0. The closer the RSSI is to 0, the better the signal is.

List of Services: A list of services that are provided by the peripheral. In the BLE scenario, a service is a collection of characteristics. A characteristic contains a single value and a number of descriptors. Descriptors describe the value contained in the characteristic, such as a max length or type.

The image above illustrates the hierarchy of these concepts. The line between Peripheral and Service depicts the “Peripheral has one or many Services” relationship.

The rest of this blog will describe the building blocks for creating an Android BLE central implementation, such as scanning for advertisements, processing an advertisement, connecting to a device (peripheral) and reading and writing, from and to a BLE peripheral. We will not discuss BLE in more depth, as that is not the goal of this blog.

Main act: Android implementation

Ask nicely

The first thing we need to do is to ask for permission to turn on Bluetooth on the mobile device and use it. For Android versions lower than 6.0, we only need to add the permissions to the Android Manifest:

<uses-permission android:name=“android.permission.BLUETOOTH"/> (ble_permission.png)

For BLE on Android 6.0 or higher, we noticed that our app was crashing when trying to use the Bluetooth functionality. It turns out that you need to add one of the following permissions to the Android Manifest:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

These permissions are required for determining the rough or precise location of the mobile device respectively. You might be wondering: Why do I need these permissions that are related to location to turn on BLE? Good question. I believe it has something to do with the fact that the signal strength of your mobile device to a BLE peripheral can be used in combination with other sources to determine your location. I have not yet looked further into this. However in the MACE application we do use the RSSI value to estimate how far you are from the MACE reader, so it is not implausible.

As of Android 6.0, users now get prompted on whether to give an app certain permissions while the app is running, instead of simply accepting all permissions when installing the app. We have to change our code to ask the user for this permission the first time they try to use the BLE functionality in the app:

if (activity.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
activity.requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
}

This code checks if the permission is already granted. If that is not the case, the user will be prompted with the request to give the ACCESS_COARSE_LOCATION permission. To handle the result of this action, you need to override the onRequestPermissionResult method. See this page for more info.

In case Bluetooth is turned off on the mobile device, we also need to ask the user if we can turn it on. To do this, we need an instance of the Bluetooth adapter. We will discuss this is the next section.

Bluetooth turn-ons

Now that we have politely asked for permission from the user to use Bluetooth, it is time to get to work. The first thing we should do is get the Bluetooth Adapter instance. The Bluetooth Adapter instance can be retrieved using the BluetoothManager. If there is no BluetoothManager present, this means that the device does not support Bluetooth. An example:

BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager == null){
//Handle this issue. Report to the user that the device does not support BLE
} else {
BluetoothAdapter adapter = bluetoothManager.getAdapter();
}

Once we have the adapter, we can get the party started. As mentioned before, we first need to check if Bluetooth is turned on for the mobile device. If this is not the case, we will ask the user to turn this on. This can be done in the following way:

if(adapter != null && !adapter.isEnabled()){
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
activity.startActivityForResult(intent, 1);
}else{
System.out.println("BLE on!");
//do BLE stuff
}

The handling of this request is done in the same way as when we asked for the permissions in the previous section. From this point on, we know that BLE is up and running on the mobile device.

Hello…is it me you’re looking for?

It’s time to start scanning! When we start the scanner, the mobile device will start receiving advertisements of BLE peripherals around it (range varies per device).

With the BluetoothAdapter we retrieved in the previous section, you can get an instance of a BLE scanner. We need to supply this scanner with at least an implementation of a ScanCallback, which describes what we should do with the results of the scan. Optionally, we can also supply the scanner with filters and settings. The filters help us specify our search in order to find the devices we are looking for. The settings are used to determine how the scanning should be performed.

This is a simple example of how to do all of this and get the scanner running:

public void startScanning(){

BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
ScanSettings scanSettings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
List<ScanFilter> scanFilters = Arrays.asList(
new ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("some uuid"))
.build());

scanner.startScan(scanFilters, scanSettings, new MyScanCallback());
}

public class MyScanCallback extends ScanCallback {

@Override
public void onScanResult(int callbackType, final ScanResult result) {
//Do something with results
}

@Override
public void onBatchScanResults(List<ScanResult> results) {
//Do something with batch of results
}

@Override
public void onScanFailed(int errorCode) {
//Handle error
}
}

A few things to note:

-There are a few scan modes available. SCAN_MODE_LOW_LATENCY has the highest frequency of scanning and will thus cause more battery drain. SCAN_MODE_LOW_POWER has the lowest frequency and is best for battery usage. This mode would mostly be used for background scanning or if a low frequency of results is acceptable for your application. SCAN_MODE_BALANCED is somewhere in between the previously mentioned modes. SCAN_MODE_OPPORTUNISTIC is a special scan mode, where the application itself does not scan but instead listens in on other applications scan results.

-The scan filter in this case is used for only detecting advertisements that contain a certain uuid as a service. All other advertisements will be ignored.

-The ScanCallback has a onBatchScanResults function, which is only called when a flush method is called on the scanner. The scanner has the ability to queue scan results before calling the callback method, however I have not looked further into this.

-In a real application, you might want to keep a reference to the scanner so that you can stop it from scanning when this is necessary.

So many advertisements…

A ScanResult object is returned for each advertisement scanned. The frequency of advertisements received is dependent on the scan mode. The scan callback will get called for each advertisement. This can become overwhelming. In the MACE project, we stop scanning as soon as we find a device we want to connect with.

The ScanResult object contains some information on the Device such as the BLE address, the service uuids and the name given to the device. The ScanResult object also contains the RSSI, which tells you roughly how close the mobile device is to the peripheral device the advertisement belongs to.

With the information in the ScanResult it is possible to do a second-level filtering, such as checking if the peripheral is close enough using RSSI or checking if the device name is what you expect it to be. If everything checks out, we can proceed to connecting to the peripheral.

Give it everything you gatt!

The concept of two BLE devices communicating with each other via services and characteristics is called Generic Attribute Profile, GATT for short. In order to use this in the mobile application, we need a BluetoothGatt instance. This can be obtained in the following way:

@Override
public void onScanResult(int callbackType, final ScanResult result) {
BluetoothDevice device = adapter.getRemoteDevice(result.getDevice().getAddress());
BluetoothGatt gatt = device.connectGatt(mContext, false, new myGattCallBack());
}

We first get an instance of the device using the BLE address obtained in the scan result. Using this device object we then call the connectGatt function, which gives us a Bluetooth Gatt instance.

The first parameter in the connectGatt method is the Android App context. The second parameter is indicates whether or not we should automatically connect to the device once it appears. This concept is also known as having devices paired with each other. Since we have commitment issues, we set this to false.

The last parameter is the callback which is called by the BluetoothGatt instance when there is a response from the peripheral. The callback should extend the BluetoothGattCallback class.

You should create your own implementation and override (at least) the following methods:

-onConnectionStateChange(BluetoothGatt gatt, int status, int newState)

-onServiceDiscovered(BluetoothGatt gatt, int status)

-onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)

Once you call the connectGATT method, the onConnectionStateChange method will be called. If the newState value is 2 (Connected), we can carry on. If this is not the case, then we cannot communicate with the peripheral and all other operations on the gatt instance will fail. Assuming we have connected successfully with the peripheral device, we can now trigger the service discovery by calling:

gatt.discoverServices();

which asks the peripheral device for a list of all services. These services are then loaded into the gatt instance.  The onServiceDiscovered method will be called with a status of 0 if it was successful. If this is the case, we can call:

List<BluetoothGattService> services = gatt.getServices();

since the gatt instance now has these services after the discovery. Note that these services also contain a list of characteristics. From this point on it is a matter of finding the right characteristic you are looking for within the right service to read from or write to.

These are not the characteristics you are looking for…

It is important to make an agreement as to which UUIDS will be used for which services and characteristics on the peripheral device. These UUIDS need to be known on the mobile application. Using these UUIDS, we can loop through the services in the following way:

private String serviceUUID = "xyz";
private String characteristicUUID = "xyz";
private BluetoothGattCharacteristic characteristic = null;

private void findCharacteristic(List<BluetoothGattService> services){
for(BluetoothGattService service: services){
if(service.getUuid().toString().equalsIgnoreCase(serviceUUID){
for(BluetoothGattCharacteristic serviceCharacteristic : service.getCharacteristics()){
if(serviceCharacteristic.getUuid().toString().equalsIgnoreCase(characteristicUUID)) {
characteristic = serviceCharacteristic;
}
}
}
}
}

Now we actually have an object that represents the characteristic that we want to read from or write to.

Change the world around you

We have reached the final step in this blog. What we want to do is finally read from and write to a peripheral device. We do this by using the characteristic object obtained in the previous section in combination with our GATT instance. To read a data in the characteristic, we simply call:

boolean successfullyRead = gatt.readCharacteristic(characteristic);

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status){
byte[] characteristicValue = characteristic.getValue();
//Do something with the value
}

The onCharacteristicRead method is called with the characteristic. You can get the value in the characteristic by calling characteristic.getValue(), which returns a byte array with the value that the characteristic contains. To write to the characteristic, we first have to set the value we want to write into the characteristic object we obtained. Once this is set, we can call write characteristic using the gatt instance:

byte[] valueToWrite = new byte[8];
Arrays.fill(valueToWrite, (byte) 0x00);
characteristic.setValue(valueToWrite);
boolean successfullyWritten = gatt.writeCharacteristic(characteristic);

The boolean successfullyWritten is true if the write action was successful, otherwise it returns false.

Epilogue

You now have the basic building blocks to build an Android BLE central implementation. What you do from here on out is up to your imagination. Of course we couldn’t cover everything in this blog. Certain things such as threading, error handling and battery optimisation were omitted in the hope to keep this entry concise. These topics will need to be covered in another blog.

In any case, you now know the essentials. Have fun and remember, scan responsibly.