Bluetooth Low Energy (or LE) is a very cool technology. The possibilities it enables for the connected world via mobile devices are simply amazing. Thanks to my friends over at NewCircle, we recorded a video tutorial for building this new technology into your Android applications.
Note: The full source code for these demos are available on GitHub.
BLE 101
Before we jump too far into some examples, let’s all get on the same page with some terms. Bluetooth LE devices are an extension of the classic Bluetooth stack that implement a specific BT profile known as the Generic Attribute (or GATT) profile.
The GATT profile defines a client/server relationship in which the server devices provide the data they have (their sensor data, for example) as characteristics that are grouped together into logical functions called services. Some characteristics are read-only, while others can be written for device configuration purposes.
Each characteristic, in addition to its primary value, may have one or more descriptors we can use to configure specific behaviors like notifications. Characteristic notifications allow us to configure a peripheral device to push updates to us, either on some schedule or when the value of the characteristic changes. This is a powerful way of reducing power usage because it doesn’t require the host application to continually poll the remote peripheral.
There are four general device roles that a device can implement in the Bluetooth LE paradigm:
- Peripheral
- Server device that provides data to clients in the form of a GATT table.
- Central
- Client device that connects to peripherals to read/write the data they provide.
- Broadcaster
- Server device that doesn’t accept incoming connections but broadcasts the data it has using advertisements.
- Observer
- Client device that scans and parses broadcast data but doesn’t initiate connections.
Currently, Android devices only have the capacity to implement either the Central or Observer roles because the APIs in Android do not fully support creating and publishing a GATT server or advertisement packet structure (yet). Let’s take a look at an example of each mode.
Finding Devices
Regardless of the roles we implement, devices must first discover each other.
All Bluetooth communication on Android starts from a BluetoothAdapter
instance.
This has changed slightly as of 4.3; prior to this, the instance was accessed directly
via BluetoothAdapter.getInstance()
, but now a new system service entitled
BluetoothManager
wraps access to the adapter.
private BluetoothAdapter mBluetoothAdapter;
//Old and busted method
mBluetoothAdapter = BluetoothAdapter.getInstance();
//New hotness method
BluetoothManager manager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
mBluetoothAdapter = manager.getAdapter();
To initiate a scan for nearby LE devices, we call startLeScan()
with a reference
to a BluetoothAdapter.LeScanCallback
where scan results will be delivered via
the onLeScan()
callback.
GATT Peripheral
Our first demo uses the TI SensorTag development platform to create a simple weather station application on the Nexus 7. The SensorTag is a feature-packed platform that exposes 6 unique sensor elements via Bluetooth LE services (each sensor is represented by a service). We end up reading three values from two of the sensor services on the tag: temperature, relative humidity, and barometric pressure; the temperature and pressure both come from the barometric pressure service.
Once we’ve discovered the peripheral we want, we need to establish a connection and discover the services on that remote device.
private LeScanCallback mScanCallback = new LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
//For the device we want, make a connection
device.connectGatt(this, true, mGattCallback);
}
};
private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
//Connection established
if (status == BluetoothGatt.GATT_SUCCESS
&& newState == BluetoothProfile.STATE_CONNECTED) {
//Discover services
gatt.discoverServices();
} else if (status == BluetoothGatt.GATT_SUCCESS
&& newState == BluetoothProfile.STATE_DISCONNECTED) {
//Handle a disconnect event
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//Now we can start reading/writing characteristics
}
}
Each sensor has two primary characteristics – one for the sensor’s current value, and another for sensor configuration which we used to enable the sensor element. We had to do this because the sensors are all disabled by default to save power, so we must explicitly enable those that we want to read by writing a specific value to the configuration characteristic for each service. This value, as well as the configuration behavior, is defined specifically by the SensorTag and not by any Bluetooth specification.
private void enableNextSensor(BluetoothGatt gatt) {
BluetoothGattCharacteristic characteristic;
//Values to set determined by SensorTag docs
switch (mState) {
case 0:
characteristic = gatt.getService(PRESSURE_SERVICE)
.getCharacteristic(PRESSURE_CONFIG_CHAR);
characteristic.setValue(new byte[] {0x02});
break;
case 1:
characteristic = gatt.getService(PRESSURE_SERVICE)
.getCharacteristic(PRESSURE_CONFIG_CHAR);
characteristic.setValue(new byte[] {0x01});
break;
case 2:
characteristic = gatt.getService(HUMIDITY_SERVICE)
.getCharacteristic(HUMIDITY_CONFIG_CHAR);
characteristic.setValue(new byte[] {0x01});
break;
default:
return;
}
//Execute the write
gatt.writeCharacteristic(characteristic);
}
Additionally, the sensor’s data characteristic supports notifications.
This allows us to enable the tag to push updates to our application by setting
the ENABLE_NOTIFICATON_VALUE
flag on the characteristic’s configuration descriptor.
This removes the need for us to continuously poll the value from our application code.
private void setNotifyNextSensor(BluetoothGatt gatt) {
BluetoothGattCharacteristic characteristic;
switch (mState) {
case 0:
characteristic = gatt.getService(PRESSURE_SERVICE)
.getCharacteristic(PRESSURE_CAL_CHAR);
break;
case 1:
characteristic = gatt.getService(PRESSURE_SERVICE)
.getCharacteristic(PRESSURE_DATA_CHAR);
break;
case 2:
characteristic = gatt.getService(HUMIDITY_SERVICE)
.getCharacteristic(HUMIDITY_DATA_CHAR);
break;
default:
return;
}
//Enable local notifications
gatt.setCharacteristicNotification(characteristic, true);
//Enabled remote notifications
BluetoothGattDescriptor desc = characteristic.getDescriptor(CONFIG_DESCRIPTOR);
desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(desc);
}
Notice that this was a two-step process. We had to locally enable notifications
in the Android API for the characteristic but also set the flag and send that
value over the to peripheral. With this flag set, we can expect regular calls to
BluetoothGattCallback.onCharacteristicChanged()
, as the SensorTag reads new
data from the sensors we’ve enabled.
In order to properly display the raw contents from the sensor services as physical
values, we’ve added a SensorTagData
utility class to do the conversions.
These formulas can be found with more detail on the
TI SensorTag’s Wiki Page.
Broadcaster
Unlike classic Bluetooth, which puts a "discoverable" device into a listening mode for requests from an active scanning device, Bluetooth LE devices "advertise" their presence actively, and the scanning device is listening for those packets. All devices do this, even the SensorTag in our previous example. It just so happens that some device configurations take more advantage of the advertisement payload than others.
The advertisement payload can be broken down into a collection of Advertisement Data (AD) structures, each composed of a length, type, and data. The type identifiers for these structures are typically assigned numbers defined by the Bluetooth Special Interest Group.
Our second demo uses connection-less temperature beacons designed and manufactured by KS Technologies. These beacons are outfitted with a single ambient temperature sensor and report that sensor’s value with each advertisement. These devices have a significantly simpler API as all of the data they provide is broadcasted out inside of their advertisement packets; no connections or characteristics are required.
Beacon devices—like ours from KST—insert their sensor information into a Service Data AD Structure that we can read out during the scanning process.
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
/*
* We need to parse out of the AD structures from the scan record
*/
List<AdRecord> records = AdRecord.parseScanRecord(scanRecord);
if (records.size() == 0) {
Log.i(TAG, "Scan Record Empty");
} else {
Log.i(TAG, "Scan Record: "
+ TextUtils.join(",", records));
}
/*
* Create a new beacon from the list of obtains AD structures
* and pass it up to the main thread
*/
TemperatureBeacon beacon = new TemperatureBeacon(records, device.getAddress(), rssi);
}
We see that, for each scan, the list of AD Structures must be parsed out.
This can be handled simply by parsing through the bytes in the record, looking
at the length and type parameters. The scanRecord
element we receive from the
APIs is typically always the same size, and padded with zeros on the end, so we
must also look for zero values in our first fields to determine when we’ve found
all the structures present.
public class AdRecord {
// ...
public static List<AdRecord> parseScanRecord(byte[] scanRecord) {
List<AdRecord> records = new ArrayList<AdRecord>();
int index = 0;
while (index < scanRecord.length) {
int length = scanRecord[index++];
//Done once we run out of records
if (length == 0) break;
int type = scanRecord[index];
//Done if our record isn't a valid type
if (type == 0) break;
byte[] data = Arrays.copyOfRange(scanRecord, index+1, index+length);
records.add(new AdRecord(length, type, data));
//Advance
index += length;
}
return records;
}
// ...
}
In the example, we created a simple beacon element that pulled out from the list of AD structures the two relevant pieces we wanted to display: the device name and the temperature value. The temperature record is stored in a Service Data structure which is prefaced with the 16-bit UUID of the service. Since there may be multiple Service Data records in the collection, we validate that we have found the record for a thermometer (temperature) service.
In this case, the math for converting temperature is also much simpler, as KST has done the longer math and returns the temperature value back already scaled to degrees Celsius.
public TemperatureBeacon(List<AdRecord> records, String deviceAddress, int rssi) {
mSignal = rssi;
mAddress = deviceAddress;
for(AdRecord packet : records) {
//Find the device name record
if (packet.getType() == AdRecord.TYPE_NAME) {
mName = AdRecord.getName(packet);
}
//Find the service data record that contains our service's UUID
if (packet.getType() == AdRecord.TYPE_SERVICEDATA
&& AdRecord.getServiceDataUuid(packet) == UUID_SERVICE_THERMOMETER) {
byte[] data = AdRecord.getServiceData(packet);
/*
* Temperature data is two bytes, and precision is 0.5degC.
* LSB contains temperature whole number
* MSB contains a bit flag noting if fractional part exists
*/
mCurrentTemp = (data[0] & 0xFF);
if ((data[1] & 0x80) != 0) {
mCurrentTemp += 0.5f;
}
}
}
}
Final Notes
BluetoothGatt
callbacks do not happen on the main thread. If you need to update the UI from one of these methods, you need to use aHandler
or other synchronization mechanism to do your updates.- Reading and writing GATT characteristics should be done one at a time. The Android APIs do not enjoy multiple write or read request being posted at once. This is why our example built a simple state machine to handle the characteristics we needed.
- LE scans do not currently report all the advertisements from a beacon, only the first one.
In order to implement an application that relies on processing data in advertisements,
you will stop and start scans on some duty cycle to get new advertisements to
deliver via
onLeScan()
.
To get your hands on some of these BLE Beacons, head over to KS Technologies. To learn more about Android training courses I’m teaching, head over to NewCircle.