Evothings BLE API Guide

This guide shows you how to use the Evothings BLE API to connect to Bluetooth Low Energy devices from JavaScript.

Please note that this guide applies to Evothings Viewer 1.5 or later, and the Cordova BLE plugin version 2.0.0 or later (included with Evothings Viewer).

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.

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 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.ble.startScan(
    onDeviceFound,
    onScanError)

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

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

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

evothings.ble.startScan(
    onDeviceFound,
    onScanError)

// Here we check for a specific service UUID advertised by the device.
function onDeviceFound(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 onDeviceFound will be called only for devices
// that advertise the given service UUID.
evothings.ble.startScan(
    onDeviceFound,
    onScanError,
    { serviceUUIDs: ['0000aa10-0000-1000-8000-00805f9b34fb'] })

function onDeviceFound(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 onDeviceFound(device)
{
    console.log('Found device:' + JSON.stringify(device.advertisementData))
}

You can also print the entire device structure:

function onDeviceFound(device)
{
    console.log('Found device:' + 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.ble.startScan(
    onDeviceFound,
    onScanError)

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

        // Connect.
        evothings.ble.connectToDevice(
            device,
            onConnected,
            onDisconnected,
            onConnectError)
    }
}

// Called when device is connected.
function onConnected(device)
{
    console.log('Connected to device')
}

// Called if device disconnects.
function onDisconnected(device)
{
    console.log('Disconnected from device')
}

// Called when a connect error occurs.
function onConnectError(error)
{
    console.log('Connect error: ' + error)
}
By default, the function evothings.ble.connectToDevice reads all services, characteristics and descriptors from the device.

Optionally, you can specify the services to read (reading only the services you need can improve performance):

evothings.ble.connectToDevice(
    device,
    onConnected,
    onDisconnected,
    onConnectError)
    { 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.

Use functions evothings.ble.getService and evothings.ble.getCharacteristic to get the characteristic you want to read and write. To read and write descriptors, use function evothings.ble.getDescriptor to get the descriptor to write/read.

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'

// Get Luxometer service and characteristics.
var service = evothings.ble.getService(device, LUXOMETER_SERVICE)
var configCharacteristic = evothings.ble.getCharacteristic(service, LUXOMETER_CONFIG)
var dataCharacteristic = evothings.ble.getCharacteristic(service, LUXOMETER_DATA)

// Turn Luxometer ON.
evothings.ble.writeCharacteristic(
    device,
    configCharacteristic,
    new Uint8Array([1]),
    onLuxometerActivated,
    onLuxometerActivatedError)

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

    // Enable notifications from the Luxometer.
    evothings.ble.readCharacteristic(
        device,
        dataCharacteristic,
        onLuxometerRead,
        onLuxometerReadError)
}

function onLuxometerActivatedError(error)
{
    console.log('Luxometer activate error: ' + error)
}

function onLuxometerRead(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 onLuxometerReadError(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:

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

    // Enable notifications from the Luxometer.
    evothings.ble.enableNotification(
        device,
        dataCharacteristic,
        onLuxometerNotification,
        onLuxometerNotificationError)
}

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

function onLuxometerNotificationError(error)
{
    console.log('Luxometer notification error: ' + error)
}

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.

Source code of file index.html (drag this file into Evothings Workbench and click RUN to launch in Evothings Viewer):

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no,
    shrink-to-fit=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0" />
<title>BLE Plugin API Demo</title>
<script>
// Redirect console.log when running from Evothings Workbench.
if (window.hyper && window.hyper.log) { console.log = hyper.log }
</script>
</head>

<body>

<h1>BLE Plugin API Demo</h1>
<p>View output in the console log.</p>

<script src="cordova.js"></script>
<script>
// 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 sensor OFF).
var LUXOMETER_CONFIG = 'f000aa72-0451-4000-b000-000000000000'

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

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

    // Start scanning. Two callback functions are specified.
    evothings.ble.startScan(
        onDeviceFound,
        onScanError)

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

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

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

            // Connect.
            connectToDevice(device)
        }
    }

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

function connectToDevice(device)
{
    evothings.ble.connectToDevice(
        device,
        onConnected,
        onDisconnected,
        onConnectError)

    function onConnected(device)
    {
        console.log('Connected to device')

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

    // Function called if the device disconnects.
    function onDisconnected(error)
    {
        console.log('Device disconnected')
    }

    // Function called when a connect error occurs.
    function onConnectError(error)
    {
        console.log('Connect error: ' + error)
    }
}

function enableLuxometerNotifications(device)
{
    // Get Luxometer service and characteristics.
    var service = evothings.ble.getService(device, LUXOMETER_SERVICE)
    var configCharacteristic = evothings.ble.getCharacteristic(service, LUXOMETER_CONFIG)
    var dataCharacteristic = evothings.ble.getCharacteristic(service, LUXOMETER_DATA)

    // Turn Luxometer ON.
    evothings.ble.writeCharacteristic(
        device,
        configCharacteristic,
        new Uint8Array([1]),
        onLuxometerActivated,
        onLuxometerActivatedError)

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

        // Enable notifications from the Luxometer.
        evothings.ble.enableNotification(
            device,
            dataCharacteristic,
            onLuxometerNotification,
            onLuxometerNotificationError)
    }

    function onLuxometerActivatedError(error)
    {
        console.log('Luxometer activate error: ' + error)
    }

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

    function onLuxometerNotificationError(error)
    {
        console.log('Luxometer notification 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>

</body>
</html>

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