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 Bluetooth Low Energy 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

LEARN MORE

Contact us if you have any questions regarding the article or just want to chat about technology, our services, job offers and more!

POLIDEA NEWSLETTER

Sign in and expect sharp insights, recommendations, ebooks and fascinating project stories delivered to your inbox

The controller of the personal data that you are about to provide in the above form will be Polidea sp. z o.o. with its registered office in Warsaw at ul. Przeskok 2, 00-032 Warsaw, KRS number: 0000330954, tel.: 0048 795 536 436, email: hello@polidea.com (“Polidea”). We will process your personal data based on our legitimate interest and/or your consent. Providing your personal data is not obligatory, but necessary for Polidea to respond to you in relation to your question and/or request. If you gave us consent to call you on the telephone, you may revoke the consent at any time by contacting Polidea via telephone or email. You can find detailed information about the processing of your personal data in relation to the above contact form, including your rights relating to the processing, HERE.

Data controller:

The controller of your personal data is Polidea sp. z o.o. with its registered office in Warsaw at ul. Przeskok 2, 00-032 Warsaw, KRS number: 0000330954, tel.: [0048795536436], email: [hello@polidea.com] (“Polidea”)

Purpose and legal bases for processing:

 

Used abbreviations:

GDPR – Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016
on the protection of natural persons with regard to the processing of personal data and on the free movement
of such data, and repealing Directive 95/46/EC (General Data Protection Regulation)

ARES – Polish Act on Rendering Electronic Services dated 18 July 2002

TL – Polish Telecommunications Law dated 16 July 2004

1)        sending to the given email address a newsletter including information on Polidea’s new projects, products, services, organised events and/or general insights from the mobile app business world |art. 6.1 a) GDPR, art. 10.2 ARES and art. 172.1 TL (upon your consent)

Personal data:name, email address

2)       statistical, analytical and reporting purposes |art. 6. 1 f) GDPR (based on legitimate interests pursued by Polidea, consisting in analysing the way our services are used and adjusting them to our clients’ needs, as well as developing new services)

Personal data:name, email address

Withdrawal of consent:

You may withdraw your consent to process your personal data at any time.

Withdrawal of the consent is possible solely in the scope of processing performed based on the consent. Polidea is authorised to process your personal data after you withdraw your consent if it has another legal basis for the processing, for the purposes covered by that legal basis.

Categories of recipients:

Your personal data may be shared with:

1)       authorised employees and/or contractors of Polidea

2)       persons or entities providing particular services to Polidea (accounting, legal, IT, marketing and advertising services) – in the scope required for those persons or entities to provide those services to Polidea

 

Retention period:

1)       For the purpose of sending newsletter to the given email address – for as long as the relevant consent is not withdrawn

2)       For statistical, analytical and reporting purposes – for as long as the relevant consent is not withdrawn

Your rights:

 

Used abbreviation:

GDPR – Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016
on the protection of natural persons with regard to the processing of personal data and on the free movement
of such data, and repealing Directive 95/46/EC (General Data Protection Regulation)

According to GDPR, you have the following rights relating to the processing of your personal data, exercised by contacting Polidea via [e-mail, phone].

1)       to access to your personal data (art. 15 GDPR) by requesting sharing and/or sending a copy of all your personal data processed by Polidea

2)       to request rectification of inaccurate personal data
(art. 16 GDPR) by indicating the data requiring rectification

3)       to request erasure of your persona data (art. 17 GDPR); Polidea has the rights to refuse erasing the personal data in specific circumstances provided by law

4)       to request restriction of processing of your personal data (art. 18 GDPR) by indicating the data which should be restricted

5)       to move your personal data (art. 20 GDPR) by requesting preparation and transfer by Polidea of the personal data that you provided to Polidea to you or another controller in a structured, commonly used machine-readable format

6)       to object to processing your personal data conducted based on art. 6.1 e) or f) GDPR, on grounds relating to your particular situation (art. 21 GDPR)

7)       to lodge a complaint with a supervisory authority,
in particular in the EU member state of your habitual residence, place of work or place of the alleged infringement if you consider that the processing
of personal data relating to you infringes the GDPR
(art. 77.1 GDPR)

No obligation to provide data:

Providing your personal data is not obligatory, but necessary for Polidea to provide you the newsletter service

Refusal to provide the above data will result in inability to receive the newsletter service.

Profiling

In the process of providing the newsletter service, we make decisions in an automated way, including profiling, based on the data you provide.

 

“Profiling” means automated processing of personal data consisting of the use of your personal data to evaluate certain personal aspects relating to you, in particular to analyze or predict aspects concerning your personal preferences and interests.

 

The automated decisions are taken based on the analysis of clicked and viewed content. They affect the targeting of specific newsletter content to selected users registered to receive the newsletter service, based on the anticipated interests of the recipient.