How to connect to BLE devices

This guide shows you how to connect to Bluetooth Low Energy devices from JavaScript.

We take you through the steps to detect and connect to a BLE device. You can use this guide for basically any BLE device.

Software architecture

Native support for Bluetooth Low Energy on Android and iOS is provided by the Cordova BLE plugin. This plugin is included with Evothings Viewer, which makes it quick to get started with development of a BLE application in JavaScript. When the app is ready for testing and deployment, the Cordova build system is used to build a custom app with the application code and the BLE plugin. This app can then be published on the app stores.

In this guide we will use a high-level BLE library called EasyBLE, which encapsulates the Cordova plugin functions. This library is available as a single file named easyble.dist.js that you include in the index.html file of your application. Code examples below use EasyBLE.

To summarise, we use two pieces of software, the Cordova BLE plugin and the EasyBLE library.

Steps to connect to a BLE device

The following are the steps needed to connect to a BLE device and start reading/writing data:

  • Scan for the device
  • Connect to the device
  • Read services and characteristics you wish to use
  • Read and write characteristics

A characteristic is like a command or function, it is a place you read or write to fetch data or control the device (turning on or off a LED or a motor, for example). Services are collections of characteristics, they group related commands/functions.

What you need to know to program a BLE device are the UUIDs of the services and characteristics you wish to use. This information is typically found in the documentation from the device manufacturer. Use these UUIDs in the code when making calls to the BLE library functions. Below we show code examples of how to do this.

How to talk with a BLE device - services and characteristics

A BLE device exposes its communication interface through services and characteristics. A service can have one or more characteristics. Each characteristic can also have one or more descriptors (descriptors tend to be accessed less frequently by application code).

For example, a BLE-enabled thermometer typically has a temperature service that has a characteristic that allows you to read the temperature.

Each service and characteristic has a universally unique id (UUID). You use these ids when setting up the communication with the BLE device from JavaScript. The UUIDs are usually found in the device documentation provided by the manufacturer.

An example of device UUIDs

TI SimpleLink Multi-Standard CC2650 SensorTag

As an example, we will use the TI SensorTag CC2650 and the Luxometer Service. The CC2650 has several sensors, each sensor has a service, each service has characteristics for turning a sensor on/off, setting the update interval, and for reading data.

Other BLE devices will have difference services and characteristics, but the general principle is the same. Some devices have options to configure the device, others may not need any specific configuration. Some devices you primarily read data from (like a thermometer), others you primarily write data to (like remotely controlled machinery).

The Luxometer of the TI SensorTag CC2650 has the following UUIDs:

Luxometer service UUID:f000aa70-0451-4000-b000-000000000000
Sensor on/off characteristic UUID:f000aa71-0451-4000-b000-000000000000
Sensor update interval characteristic:f000aa73-0451-4000-b000-000000000000
Sensor data characteristic:f000aa71-0451-4000-b000-000000000000

These UUIDs are used in the example code shown below. Use the UUIDs for the services and characteristics of the device yo are working with in your actual application code.

Debug printing in Evothings Studio

In the code examples below, console.log() is used to print debug data. To output log statements in Evothings Workbench, you can map console.log to hyper.log, as follows:

// Redirect console.log to Evothings Workbench.
if (window.hyper && window.hyper.log) { console.log = hyper.log }

Log data is output in the Console Window in Evothings Workbench. Click the button named "Console" in the Workbench to open this window.

How to find a BLE device

Scanning for name and service UUID

First step when communicating with any BLE device is to establish a connection to it, and to do that you first need to find the device. This step is called scanning. When scanning for devices, you will get all BLE devices within range. This means you must somehow find out which device is the one you wish to connect to.

To determine the identity of a device using scanning, you can use several methods. Two useful ways are to use the name of the device and/or the advertised service UUIDs of the device.

Here is how to find the TI SensorTag CC2650 by its name:

// Start scanning. Two callback functions are specified.
evothings.easyble.startScan(
    deviceFound,
    scanError)

// This function is called when a device is detected, here
// we check if we found the device we are looking for.
function deviceFound(device)
{
    if (device.getName() == 'CC2650 SensorTag')
    {
        console.log('Found the TI SensorTag!')
    }
}

// Function called when a scan error occurs.
function scanError(error)
{
    console.log('Scan error: ' + error)
}

By comparison, here is how find the device by advertised service UUID:

evothings.easyble.startScan(
    deviceFound,
    scanError)

// Here we check for a specific service UUID advertised by the device.
function deviceFound(device)
{
    // kCBAdvDataServiceUUIDs is an array of strings with service UUIDs.
    var advertisedServiceUUIDs = device.advertisementData.kCBAdvDataServiceUUIDs
    if (advertisedServiceUUIDs.indexOf('0000aa10-0000-1000-8000-00805f9b34fb') > -1)
    {
        console.log('Found the TI SensorTag!')
    }
}

You can also specify the service UUID in the options parameter to startScan, this will instruct the native BLE system to report only devices that advertises the given service UUID:

// The callback function deviceFound will be called only for devices
// that advertise the given service UUID.
evothings.easyble.startScan(
    deviceFound,
    scanError,
    { serviceUUIDs: ['0000aa10-0000-1000-8000-00805f9b34fb'] })

function deviceFound(device)
{
    console.log('Found the TI SensorTag!')
}

Let the user select which device to connect to

An approach to selecting which device to connect to is to display a list of devices and their names in the application user interface, and let the user select a device. The list is dynamically constructed during scanning, and can be updated and sorted by distance or by RSSI value.

Using advertisement data for device identification

The advertised service UUIDs (kCBAdvDataServiceUUIDs) used above is part of the BLE advertisement data. This is a block of bytes that is broadcasted by a BLE device when it is in advertisement mode. Have a look at the advertisement data documentation for a list of available fields (note that these fields may be undefined or empty, depending on the BLE device).

It can happen that a device cannot be uniquely identified by its name and/or advertised service UUIDs. Depending on the device, there may be other data in the advertisement that can be used to identify it. Eddystone beacons, for example, broadcast ids and URLs as part of the advertisement data.

Here is how to output a printable representation of the advertisement data structure:

function deviceFound(device)
{
    console.log('Found device: ' + device.getName())
    console.log(JSON.stringify(device.advertisementData))
}

Hint: You can also print the entire device structure:

function deviceFound(device)
{
    console.log(JSON.stringify(device))
}

Connect to a device

When the device has been found, next step is to connect to it. Typically scanning is now stopped, and the connect function is called. Here is an example:

evothings.easyble.startScan(
    deviceFound,
    scanError)

// This function is called when a device is detected, here
// we check if we found the device we are looking for.
function deviceFound(device)
{
    if (device.getName() == 'CC2650 SensorTag')
    {
        // Stop scanning.
        evothings.easyble.stopScan()

        // Connect.
        device.connect(connectSuccess, connectError)
    }
}

function connectSuccess(device)
{
    console.log('Connected to device')
}

// Function called when a connect error or disconnect occurs.
function connectError(error)
{
    if (error == evothings.easyble.error.DISCONNECTED)
    {
        console.log('Device disconnected')
    }
    else
    {
        console.log('Connect error: ' + error)
    }
}

Read services and characteristics

When connected we can read services, characteristics and descriptors. This step is needed to be able to communicate with the device and read and write characteristics. Here is how this is done:

function connectSuccess(device)
{
    console.log('Connected to device, reading services...')

    // Read all services, characteristics and descriptors.
    device.readServices(readServicesSuccess, readServicesError)
}

function readServicesSuccess(device)
{
    console.log('Read services completed')

    // We can now read/write to the device.
}

function readServicesError(error)
{
    console.log('Read services error: ' + error)
}

When reading services, you can specify the services to read (this can improve the performance):

device.readServices(
    readServicesSuccess,
    readServicesError,
    { serviceUUIDs: ['f000aa70-0451-4000-b000-000000000000'] })

Reading and writing characteristics

Characteristics are like commands or functions, they can be read to obtain data, or written to control some aspect of a device.

For example, on the TI SensorTag, you turn a sensor on and off by writing to a characteristic. When the sensor is on, you can read data from another characteristic. Both characteristics are part of the same service.

Here is an example how to turn on and read the Luxometer of the TI SensorTag:

// UUID of Luxometer service.
var LUXOMETER_SERVICE = 'f000aa70-0451-4000-b000-000000000000'

// UUID of Luxometer config characteristic (write 1 to
// turn sensor ON, 0 to turn OFF).
var LUXOMETER_CONFIG = 'f000aa72-0451-4000-b000-000000000000'

// UUID of Luxometer data characteristic.
var LUXOMETER_DATA = 'f000aa71-0451-4000-b000-000000000000'

// Turn Luxometer ON.
device.writeCharacteristic(
    LUXOMETER_SERVICE,
    LUXOMETER_CONFIG,
    new Uint8Array([1]),
    turnOnLuxometerSuccess,
    turnOnLuxometerError)

// Now we can read data from the Luxometer.
device.readCharacteristic(
    LUXOMETER_SERVICE,
    LUXOMETER_DATA,
    readLuxometerSuccess,
    readLuxometerError)

function turnOnLuxometerSuccess()
{
    console.log('Luxometer is ON')

}
function turnOnLuxometerError(error)
{
    console.log('Write Luxometer error: ' + error)
}

function readLuxometerSuccess(data)
{
    // Get raw sensor value (data buffer has little endian format).
    var raw = new DataView(data).getUint16(0, true)
    console.log('Raw Luxometer value: ' + raw)
}

function readLuxometerError(error)
{
    console.log('Read Luxometer error: ' + error)
}

The data read from a device is in raw a byte format. Documentation from the device manufacturer contains information about how to calculate the actual value. As an example, here is how you calculate the Luxometer value in lux units:

// Calculate the light level from raw sensor data.
// Return light level in lux.
function calculateLux(data)
{
    // Get 16 bit value from data buffer in little endian format.
    var value = new DataView(data).getUint16(0, true)

    // Extraction of luxometer value, based on sfloatExp2ToDouble
    // from BLEUtility.m in Texas Instruments TI BLE SensorTag
    // iOS app source code.
    var mantissa = value & 0x0FFF
    var exponent = value >> 12

    var magnitude = Math.pow(2, exponent)
    var output = (mantissa * magnitude)

    var lux = output / 100.0

    // Return result.
    return lux
}

Using notifications to read data

It is common that an application needs to continuously read data from a BLE device. Examples include reading a sensor and send data to the cloud, or to use the accelerometer of a sensor device to control a game on the mobile phone.

Notifications are an efficient way to read data continuously from a BLE device. The process is similar to reading data, the main difference is that the callback function will be called repeatedly until the notification is disabled.

Here is a code example of how to enable and handle notifications:

// Enable notifications from the Luxometer.
device.enableNotification(
    LUXOMETER_SERVICE,
    LUXOMETER_DATA,
    readLuxometerSuccess,
    readLuxometerError)

// Called repeatedly until disableNotification is called.
function readLuxometerSuccess(data)
{
    var lux = calculateLux(data)
    console.log('Luxometer value: ' + lux)
}

Complete code example

Here is a complete code example. Note that file cordova.js is included with the BLE plugin, this file is not part of the application code, it is bundled with the plugin and with Evothings Viewer.

Scripts in file index.html:

<script src="cordova.js"></script>
<script src="easyble.dist.js"></script>
<script>
// UUIDs of services and characteristics.
var LUXOMETER_SERVICE = 'f000aa70-0451-4000-b000-000000000000'
var LUXOMETER_CONFIG = 'f000aa72-0451-4000-b000-000000000000'
var LUXOMETER_DATA = 'f000aa71-0451-4000-b000-000000000000'

function findDevice()
{
    console.log('Start scanning')

    // Start scanning. Two callback functions are specified.
    evothings.easyble.startScan(
        deviceFound,
        scanError)

    // This function is called when a device is detected, here
    // we check if we found the device we are looking for.
    function deviceFound(device)
    {
        console.log('Found device: ' + device.getName())

        if (device.getName() == 'CC2650 SensorTag')
        {
            console.log('Found the TI SensorTag!')

            // Stop scanning.
            evothings.easyble.stopScan()

            // Connect.
            connectToDevice(device)
        }
    }

    // Function called when a scan error occurs.
    function scanError(error)
    {
        console.log('Scan error: ' + error)
    }
}

function connectToDevice(device)
{
    device.connect(connectSuccess, connectError)

    function connectSuccess(device)
    {
        console.log('Connected to device, reading services...')

        // Read all services, characteristics and descriptors.
        device.readServices(readServicesSuccess, readServicesError)
    }

    function readServicesSuccess(device)
    {
        console.log('Read services completed')

        // Enable notifications for Luxometer.
        enableLuxometerNotifications(device)
    }

    function readServicesError(error)
    {
        console.log('Read services error: ' + error)
    }

    // Function called when a connect error or disconnect occurs.
    function connectError(error)
    {
        if (error == evothings.easyble.error.DISCONNECTED)
        {
            console.log('Device disconnected')
        }
        else
        {
            console.log('Connect error: ' + error)
        }
    }
}

function enableLuxometerNotifications(device)
{
    // Turn Luxometer ON.
    device.writeCharacteristic(
        LUXOMETER_SERVICE,
        LUXOMETER_CONFIG,
        new Uint8Array([1]),
        turnOnLuxometerSuccess,
        turnOnLuxometerError)

    // Enable notifications from the Luxometer.
    device.enableNotification(
        LUXOMETER_SERVICE,
        LUXOMETER_DATA,
        readLuxometerSuccess,
        readLuxometerError)

    function turnOnLuxometerSuccess()
    {
        console.log('Luxometer is ON')

    }
    function turnOnLuxometerError(error)
    {
        console.log('Write Luxometer error: ' + error)
    }

    // Called repeatedly until disableNotification is called.
    function readLuxometerSuccess(data)
    {
        var lux = calculateLux(data)
        console.log('Luxometer value: ' + lux)
    }

    function readLuxometerError(error)
    {
        console.log('Read Luxometer error: ' + error)
    }
}

// Calculate the light level from raw sensor data.
// Return light level in lux.
function calculateLux(data)
{
    // Get 16 bit value from data buffer in little endian format.
    var value = new DataView(data).getUint16(0, true)

    // Extraction of luxometer value, based on sfloatExp2ToDouble
    // from BLEUtility.m in Texas Instruments TI BLE SensorTag
    // iOS app source code.
    var mantissa = value & 0x0FFF
    var exponent = value >> 12

    var magnitude = Math.pow(2, exponent)
    var output = (mantissa * magnitude)

    var lux = output / 100.0

    // Return result.
    return lux
}

// Start scanning for devices when the plugin has loaded.
document.addEventListener('deviceready', findDevice, false)
</script>

Get started in 5 minutes

Download Evothings Studio and get started within minutes. It is fun and easy!

Ask questions and discuss IoT app development on Gitter: gitter.im/evothings/evothings