This entry is part [part not set] of 3 in the series Bluetooth Beacons Titanium Mobile Apps

This post is a continuation of the “Bluetooth, Beacons & Titanium Mobile Apps” series. For more details and to learn more about Bluetooth and mobile apps, you can start at the beginning of the series and read Understanding Bluetooth for Android, iOS, & Titanium.  The Bluetooth LE (“BLE”) protocol is one of the Bluetooth protocols and has been available since BT4.0. Read Part 1 of this series to better understand why saying “BT4.0” does not mean “BLE” and vice-versa.

Beacons are a specific use case of BLE so we have included building a beacons example together with a regular BLE example to understand how they are similar yet different.

This part of the series walks through building 2 simple example Bluetooth apps in Titanium for iOS and Android:

  1. BLE Heart Rate Monitor example
  2. BLE Beacons example

Communicating with a BLE Heart Rate Monitor

This example uses the BLE protocol to build an app for both Android and iOS that communicates with a Zephyr HXM-2 heart rate monitor. The example uses two modules:

  1. The Logical Labs Bluetooth LE iOS Module
  2. The Logical Labs Bluetooth LE Android Module

Steps of developing this example app:

  1. Start with a single page Alloy app from the Titanium Studio template
  2. Install the Logical Labs Bluetooth LE iOS and Android modules and add them to the app
  3. Add the UI elements
  4. Initialize the BLE components and start scanning
  5. Connect to the heart rate monitor
  6. Discover services
  7. Discover characteristics
  8. Handle connection failures and lost connections
  9. Handle heart rate measurement updates
  10. Querying battery status

Each of these steps is represented by a commit in this GitHub repo, which contains all of the code of this example. The most interesting parts of each commit are shown in subsequent sections.

Adding the BLE Module to the App

We will use alloy.js to load the module, and later the JavaScript libraries, into tho Alloy.Globals namespace:

Alloy.Globals.BluetoothLE = require('com.logicallabs.bluetoothle');

 Initialization and Scanning

In this step we add two JavaScript libraries, which we load in alloy.js:

Alloy.Globals.BLEUtils = require('ble_utils');
Alloy.Globals.HRMUtils = require('hrm_utils');

The BLEUtils library takes care of the initialization, which is platform dependent. All other code is shared between the two platforms!

In controllers/index.js we use the initialization function provided by the BLEUtils library, and start scanning when initialization is completed:

function startScan() {
    [...]
    BluetoothLE.startScan({
        UUIDs: [ HRMUtils.getServiceUuid() ]
    });
    [...]
}

BluetoothLE.addEventListener('moduleReady', function() {
    [...]
    BLEUtils.initCentral({
        onCallback: function() {
            startScan();
        },
    [...]
    });
});

 Connecting to the Heart Rate Monitor

When a heart rate monitor is discovered, we stop scanning and try to connect to it:

BluetoothLE.addEventListener('discoveredPeripheral', function(e) {
    [...]
    stopScan();
    BluetoothLE.connectPeripheral({
        peripheral: e.peripheral,
        autoConnect: false
    });
    [...]
});

It is Apple’s recommendation to stop scanning as soon as practical, preferably before you try to establish any connections. This helps with performance and stability of the connection.

When the connection is established, we store the peripheral object in a variable and attach event handlers to it:

BluetoothLE.addEventListener('connectedPeripheral', function(e) {
    HRMUtils.setConnectedPeripheral(e.peripheral);
    [...]
});
exports.setConnectedPeripheral = function(newConnectedPeripheral) {
    [...]
    connectedPeripheral = newConnectedPeripheral;
    if (connectedPeripheral) {
        connectedPeripheral.addEventListener('discoveredServices', digestServices);
        connectedPeripheral.addEventListener('discoveredCharacteristics', digestCharacteristics);
        connectedPeripheral.addEventListener('updatedValueForCharacteristic', digestNewCharValue);
    }
};

 Adding Service Discovery

Service discovery is initiated using the discoverServices function, and completed by the event handler for the discoveredServices event:

function digestServices(e) {
    var services;

    // e.source is the peripheral sending the discoveredServices event
    services = e.source.services;

    services.forEach(function(service) {
        Ti.API.info('Discovered service ' + service.UUID);
        if (BLEUtils.uuidMatch(service.UUID, HR_SERVICE_UUID)) {
            Ti.API.info('Found heart rate service!');
        }
        if (BLEUtils.uuidMatch(service.UUID, BATTERY_SERVICE_UUID)) {
            Ti.API.info('Found battery service!');
        }
    });
}

exports.setConnectedPeripheral = function(newConnectedPeripheral) {
    [...]
    connectedPeripheral.addEventListener('discoveredServices', digestServices);
    [...]
    connectedPeripheral.discoverServices();
};

Discovering Characteristics

Characteristics discovery is initiated using the discoverCharacteristics function, and completed by the event handler for the discoveredCharacteristics event. Use the subscribeToCharacteristic function to request notifications when the value of a characterisctic is updated.

function digestCharacteristics(e) {
    characteristics = e.service.characteristics;
    characteristics.forEach(function(characteristic) {
        if (BLEUtils.uuidMatch(characteristic.UUID, HR_CHAR_UUID)) {
            [...]
            connectedPeripheral.subscribeToCharacteristic(characteristic);
    [...]
}

exports.setConnectedPeripheral = function(newConnectedPeripheral) {
    [...]
    connectedPeripheral.addEventListener('discoveredCharacteristics', digestCharacteristics);
    [...]
};

 Handling Connection Failures and Lost Connections

Use the failedToConnectPeripheral and disconnectedPeripheral events to handle connection failures and lost connections. In our case the most important thing to do is to restart scanning, since we followed Apple’s recommendation and stopped scanning as soon as the heart rate monitor was discovered.

BluetoothLE.addEventListener('failedToConnectPeripheral', function(e) {
    $.peripheralStatus.update('Failed to connect');
    [...]
    startScan();
});

BluetoothLE.addEventListener('disconnectedPeripheral', function(e) {
    $.peripheralStatus.update('Disconnected');
    [...]
    startScan();
});

 Handling Heart Rate Measurement Updates

The heart rate monitor sends us periodic updates about the measured heart rate. The BLE module sends updatedValueForCharacteristic events when this happens. We verify that the update is for the heart rate measurement characteristic, and translate it to a ‘heartRateUpdate’ event.

function digestNewCharValue(e) {
    [...]
    if (BLEUtils.uuidMatch(e.characteristic.UUID, HR_CHAR_UUID)) {
        [...]
        Ti.App.fireEvent(
            'heartRateUpdate',
            {
                heartRateMeasurement: e.characteristic.value[1]
            }
        );
    }
}

exports.setConnectedPeripheral = function(newConnectedPeripheral) {
    [...]
    connectedPeripheral.addEventListener('updatedValueForCharacteristic', digestNewCharValue);
    [...]
};

 Querying Battery Status

Unlike the heart rate measurements, which are reported periodically, battery status must be polled using the readValueForCharacteristic function:

exports.checkBatteryStatus = function() {
    [...]
    connectedPeripheral.readValueForCharacteristic(batteryChar);
    [...]
};

Detecting BLE Beacons

This example shows how simple it is to build an app that detects an iBeacon. This particular example is configured with the default UUID of Estimote beacons. The extended configuration capabilities available in the Estimote SDK are not covered here. You would only need such capabilities for an app that was intended to manage or control the beacon vs. a consumer app intended to detect it as any other beacon. Contact Logical Labs directly if you are interested in these extended Estimote specific configuration capabilities for your app. This example can use either the Bluetooth LE modules from the previous example or the Beacons Module for iOS & Android (as one single product).

Steps of developing this example app:

  1. Start with a single page Alloy app from the Titanium Studio template
  2. Install the Logical Labs Beacons or BLE modules for iOS and Android and add them to the app
  3. Add the UI elements
  4. Add region monitoring
  5. Add ranging for beacons

Each of these steps is represented by a commit in this GitHub repo, which contains all of the code of this example. The most interesting parts of each commit are shown in subsequent sections.

 Adding the Module to the App

The sample code in the GitHub repo use the Beacons module:

Alloy.Globals.Beacons = require('com.logicallabs.beacons');

Alternatively, you can use the BLE module to communicate with beacons:

Alloy.Globals.BluetoothLE = require('com.logicallabs.bluetoothle');

The functionality available in the Beacons module is a subset of the functionality available in the BLE module.

Adding Region Monitoring

A region is identified by a UUID, which in this case we store in the REGION_UUID variable.

    REGION_UUID = 'B9407F30-F5F8-466E-AFF9-25556B57FE6D'

The UUID used here is the default UUID of the Estimote beacons. In a real scenario, you would want to match the UUID configured on the beacons with the UUID specified in the app.

Monitoring a region means that we scan for beacons that belong to this region and notify the app whether the user is inside or outside the region. The startRegionMonitoring and stopRegionMonitoring functions are used to start and stop the scanning, and the regionStateUpdated event is used to signal changes in the region’s state (inside or outside).

 Beacons.addEventListener('regionStateUpdated', function(e) {
    var stateStr;

    switch(e.state) {
        case Beacons.REGION_STATE_UNKNOWN:
            stateStr = 'unknown';
            break;
        case Beacons.REGION_STATE_INSIDE:
            stateStr = 'inside.';
            break;
        case Beacons.REGION_STATE_OUTSIDE:
            stateStr = 'outside.';
            break;
    }
    $.status.update('Region state is now ' + stateStr);
});

function startScan() {
    Beacons.startRegionMonitoring({
        beaconRegion: beaconRegion
    });
}

function stopScan() {
    Beacons.stopRegionMonitoring({
        beaconRegion: beaconRegion
    });
}

 

Adding Ranging for Beacons

When the app gets the notification that the user entered a region, it starts ranging for beacons in that region using the startRangingBeacons function:

function startRanging() {
    rangingActive = true;
    Beacons.startRangingBeacons({
        beaconRegion: beaconRegion
    });
}

[...]

Beacons.addEventListener('regionStateUpdated', function(e) {
    var stateStr;

    switch(e.state) {
        [...]
        case Beacons.REGION_STATE_INSIDE:
            stateStr = 'inside.';
            startRanging();
            break;
    [...]
});

 

This results in periodic rangedBeacons events, roughly one every second. The app uses these events to determine which beacons are within range and displays their major number, minor number, and RSSI value in the table:

Beacons.addEventListener('rangedBeacons', function(e) {
    [...]
    tableData = [];
    e.beacons.forEach(function(beacon) {
        tableData.push({
            title: 'Major/minor/RSSI: ' +
                    beacon.major + '/' + beacon.minor + '/' + beacon.RSSI
        });
    });
    $.table.setData(tableData);
});

 

Series Navigation