Bluetooth Smart for Android – Video Tutorial

Dave Smith
Dave Smith
Bluetooth Smart for Android – Video Tutorial

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.

Advertising Payload

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 a Handler 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.