Bluetooth LE Developer Study Guide - Hands-On Lab (Apache Cordova) V5.1
Bluetooth LE Developer Study Guide - Hands-On Lab (Apache Cordova) V5.1
Version: 5.1.0
1
Contents
REVISION HISTORY .................................................................................................................................... 4
INTRODUCTION........................................................................................................................................... 6
Overview 6
Terminology Reminder 7
Functional Requirements 8
Apache Cordova Plugins 9
Build .................................................................................................................................................... 13
Install ................................................................................................................................................... 13
Notes ................................................................................................................................................... 13
Remote Debugging 14
3
Revision History
Version Date Author Changes
2.0 3rd September 2014 Martin Woolley, Bluetooth Make a Noise button now
SIG triggers the Immediate
Alert service instead of a
custom service. Switch
added to enable/disable
sharing of client proximity
data with a new custom
service called the
Proximity Monitoring
service.
3.0.0 7th July 2016 Martin Woolley, Bluetooth Name change and version
SIG number increment to sync
with changes to the server
lab for V3.0.
3.1.0 3rd October Martin Woolley, Bluetooth Android code retired to the
SIG legacy folder and
designated 'Android 4' as it
uses the Android 4.4 APIs
which have been
deprecated.
Android lab modified to be
based on Android Studio
instead of Eclipse.
Code substantially
refactored.
Android lab and solution
now uses the Android 5+
APIs and is compatible
with Android 6+ with
respect to the new
permissions model.
Key Android Bluetooth
classes are now introduced
and explained.
New much more alarm-
like alarm sound used.
3.2.0 16th December 2016 Martin Woolley, Bluetooth iOS Objective-C resources
SIG retired and replaced with
new lab and solution
written in Swift. No
changes to the Android
4
resources.
4.0.0 7th July 2017 Martin Woolley, Bluetooth Added a new lab, with
SIG associated solution source
code, which explains how
to use the Apache Cordova
SDK to create a cross
platform mobile
application which uses
Bluetooth Low Energy.
5.0.0 19th December 2017 Martin Woolley, Bluetooth Added support for
SIG temperature monitoring
using Bluetooth
indications
5.0.2 15th June 2018 Martin Woolley, Bluetooth Removed use of Bluetooth
SIG Developer Studio
5
Introduction
Overview
This document is a step-by-step guide to creating a Bluetooth application for iOS or Android using the
Apache Cordova SDK. The application is intended to be used with the Bluetooth peripheral device
created in one of the server labs of this study guide and so ideally, you should complete one of those
labs first.
Apache Cordova is an SDK which allows developers to create mobile applications using HTML, CSS and
JavaScript and to then generate an installable build for a particular target platform, all from the same
source code. This is a long way of saying that it lets you code once but build runtime versions for many,
different platforms. We assume you will be working with an Android device, an iOS device or, if you’re
lucky, both.
In this lab we will not cover any basics of programming with web technologies. It’s assumed that you
have at least a basic understanding of HTML, CSS and JavaScript. Ideally you will also have an
appreciation of how to use JavaScript with the Document Object Model (DOM). If these topics are
completely new to you, it’s recommended that you first review a basic HTML/CSS/JavaScript tutorial
from somewhere on the web first. That said, the lab will guide you at every step of the way and you’ll be
starting out with a skeleton implementation, which will take care of all of the HTML and CSS that we
need, as well as some of the JavaScript, so the learning curve should be pretty shallow if you have no
experience of these technologies.
The lab, tasks and code that you’ll create are designed to provide instruction in the principles and
practice of developing Apache Cordova applications which use Bluetooth Low Energy technology.
Objectives
This lab provides instructions to achieve the following:
• Scan for and discover a nearby Bluetooth peripheral device.
System Requirements
Terminology Reminder
Our completed Bluetooth system will consist of two major parts; a device (#1) running some software
which will display proximity information and allow users to initiate the flashing of lights and sounding of
the buzzer in our custom electronic circuit and a device (#2) which is connected electrically to that
circuit and which controls its state based on communication it receives over Bluetooth from the other
device.
Device #1 will usually be referred to as a client or sometimes as a Central mode device. It will often be
an application running on a smartphone but could be implemented some other way, such as in a web
browser.
Device #2 will sometimes be referred to as a peripheral device, sometimes as a server and sometimes as
a GAP Peripheral. Which is used will depend on the context. They all refer to the same thing and are all
equally valid in a given context. Typically this device will be something like a BBC micro:bit or a
Raspberry Pi.
7
“Client” and “Server” come from GATT. “Peripheral” and “Central” come from GAP. “Peripheral device”
is informal. Re-read the LE Basic Theory guide if any of this is not making sense.
Functional Requirements
When finished, the application we will create in this lab will:
1 Be able to scan for and discover devices running Clicking a button will initiate Bluetooth scanning.
and advertising the custom profile we designed Filtering will ensure that only devices which are
and implemented in the server lab of this starter advertising with a device name of “BDSK” are selected
study guide. and presented to the user in a UI list for selection.
2 Allow the user to establish a Bluetooth Clicking a button will cause a Bluetooth connection to
connection between their smartphone or tablet be established. The UI should indicate in a simple way
and the selected Bluetooth peripheral device that a connection has been created.
(which is presumed to be the peripheral used
during the server lab).
3 Allow the user to set an alert level of 0, 1 or 2 to The UI will include three colour coded “alert level”
be used by the peripheral when indicating that buttons (0=green=low, 1=yellow=medium,
the Bluetooth link has been lost or when the user 2=red=high). When clicked by the user, the
explicitly commands the peripheral to make a smartphone application will write selected value of 0,
noise. 1 or 2 to the Link Loss Service’s Alert Level
characteristic.
4 Allow the user to instruct the peripheral that it The UI will include a button labelled “Make a noise”.
should make a noise and flash its lights so that it When clicked, the smartphone application will write
can be easily located if lost or hidden. the selected alert level value of 0, 1 or 2 to the
peripheral Immediate Alert Service’s Alert Level
characteristic.
6 Proximity Sharing The user must be able to enable or disable this feature
using a suitable UI switch. When enabled, the RSSI and
a distance classification of 1=near, 2=medium or 3=far
will be periodically sent to the peripheral by writing to
the Client Proximity characteristic of the custom
Proximity Monitoring service.
Getting Started
It is assumed that you have installed Apache Cordova on your PC, along with Android Studio and/or
Xcode.
Apache Cordova works on Windows, Mac and Linux. Use of the SDK is almost identical across these
platforms. The only place where your use of the SDK will vary, is when we come to building our
application, either for Android, on any platform that the Android Studio is available for, or for iOS on a
Mac.
To complete the set up of your development environment, complete the following steps, working from
within a command shell. I’ll illustrate the steps involved using Windows. If you’re working on Mac OS or
Linux, watch out for file permissions issues and prefix Cordova commands with “sudo” where necessary.
All Cordova SDK commands start with “cordova” and are followed by a series of parameters:
9
• bdsk - the name of the folder which will form the root of our project
If you’re curious, list the directory contents to get an idea of what a Apache Cordova project looks like
after it’s first created.
Next, if you wish to build your application for Android, execute the following:
If you’re working on a Mac and wish to build for iOS, execute the following instead:
Add Plugins
Apache Cordova projects make use of “plugins”, JavaScript APIs which give access to platform features
and capabilities not supported fully by standard JavaScript, as used in a web browser. We’ll be using
10
three plugins in our project, plus one more if you’re building for iOS. Execute the following commands to
install the plugins we need.
This plugin provides and API with which to scan for and connect to LE devices and then perform GATT
operations like reading and writing to characteristics.
See https://github.com/don/cordova-plugin-ble-central for full details of this plugin.
The device plugin allows you to find out various details of the device the application is executing on. We
need that so we can accommodate a difference between iOS and Android when it comes to playing an
audio file to indicate that the Bluetooth connection has dropped (Feature 7 in our requirements list).
See https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-device/index.html for full
details of this plugin.
This plugin has various capabilities relating to multimedia. We’ll be using it to play or stop playback of
an audio file whenever the Bluetooth link drops.
11
Copy the starter source code into place
Rather than code every line from scratch, we’ll be solely concerned with implementing the Bluetooth
related features and to make life easier, will be starting out with an incomplete implementation of the
lab application, with some gaps in the code which we’ll need to fill as we progress.
Delete the contents of your bdsk\www folder so that the default code which was generated for you
when Cordova created the project, is cleared.
Now, copy the contents of Apache Cordova\starter\bdsk\www in your BDSK package, into your project’s
bdsk\www folder. Your www folder should now look like this:
Figure 3 - Project www folder with starter project files copied into place
12
You will not be required to change this file.
js\utils.js Contains various “utility functions”.
You will not be required to change this file.
sounds\alarm.wav An audio recording of an alarm sound which we will use in the BDSK
application to indicate link loss.
You will not be required to change this file.
Open each of the text files in your favourite editor and have a quick look at the code, just to get
orientated. Don’t change anything yet. We’ll systematically implement one function at a time in the
exercises that follow.
Let’s begin by checking exactly what state the starter app is in before we change it in Exercise 1. Build
the application and install it. The following instructions indicate the commands you will use for these
steps. We’ll be doing this repeatedly throughout the exercises and you should be able to recall them
using your shell’s command history buffer. It will be assumed from now on, that if the lab requires you
to build and install the application, you’ll know what to do. The following instructions will not be
repeated in this lab document again.
Build
Android iOS
cordova build android sudo cordova build ios
Install
Android iOS
adb install -r Open the Xcode project which the build process
platforms\android\build\outputs\apk\android- has created for you from the platforms/ios folder.
debug.apk Build / run in Xcode with a USB-connected iOS
device as the target.
Notes
If you run into permissions problems when building on Mac OS, set all file permissions in your bdsk
project using a command similar to the following. Substitute your own user and group names for your
machine.
Launch the application on your smartphone. It should look like the screen shot in Figure 4.
13
Figure 4 - The device discovery screen
Select the Scan button. Nothing should happen because we haven’t yet implemented the associated
JavaScript function.
Remote Debugging
In fact *something* should happen. It just has no effect on the UI, so you can’t see it at this stage. Each
of the functions in the app.js file announces itself by writing a message to the JavaScript console. We can
see these messages by using a remote debugging technique, supported by the Google Chrome browser,
which is available for Windows, Mac OS and Linux or by Safari on Mac OS. This is a really useful facility
and it’s recommended that you use it throughout these exercises, to monitor execution and to help
solve problems.
With your test device (smartphone or tablet) connected to your computer over USB, proceed as
described in one of the next two sections, according to the browser you are using. Note that Android
devices must be in “developer mode” with “USB debugging” enabled in the Settings\Developer options
screen. You’ll find information on this topic elsewhere on the internet.
14
Figure 5 - Chrome and the Inspect Devices Page
Click the “inspect” link under “WebView in com.bluetooth.cordova.bdsk” and you’ll be presented with
the remote debugging page, with the screen of your connected device echoed on the left hand side of
the page and the JavaScript console on the right.
15
Figure 6 - The Google Chrome remote debugging screen
Click the Scan button a few times and you’ll see the output from the app.startScan() function.
16
Remote Debugging with Safari
To perform remote debugging using Safari, follow these steps:
17
The Bluetooth Low Energy API
We’re using a 3rd party Cordova plugin to provide us with an API for Bluetooth Low Energy operations.
You’ll find both its code and API documentation here:
https://github.com/don/cordova-plugin-ble-central
As you can see, API functions belong to JavaScript objects, so we prefix calls to particular functions with
the name of that object. We then pass parameters, as indicated in the API documentation, and typically
this will include references to two functions, as described.
The Bluetooth Low Energy API we are using in this lab is available via a JavaScript object with the name
“ble”. It has been automatically created and is available for use.
18
Exercise 1 - Scanning for peripheral devices
For this exercise you need to have your peripheral, running the code created in the server lab (or the
complete solution code which we have included). Our goal in this exercise is to give our Cordova
application the ability to scan for and list devices it finds within range that are suitable for use from this
application. We’ll find out more about exactly what this means as we progress through the steps which
follow.
As you can see from Figure 4, clicking the Scan button triggers a call to a JavaScript function called
app.findDevices(). Figure 7, repeated here for convenience as Figure 9,
Exercise one consists of two stages. In the first stage, we’ll make the Scan button discover any and all
Bluetooth Low Energy devices which are in range and advertising and we’ll list them all on the Scan
19
screen. In the second stage, we’ll improve the user experience by filtering the list of discovered devices
so that only the peripheral BDSK device is selected for inclusion in the device list within our UI.
2. Device Discovery
We have four tasks to complete, here.
• Define a success function
• Define an error function
• Initiate scanning for 5 seconds
• Check whether or not any devices were discovered during scanning
Find the startScan() function in app.js and add the following code under the first TODO comment.
This function will be called each time a new, advertising device is discovered. It receives a JSON object
containing details of the discovered device, as a parameter. We log the peripheral object and examples
look like this:
{"name":"Kontakt","id":"C5:EA:CF:9A:8F:73","advertising":{},"rssi":-78}
{"name":"BDSK","id":"98:4F:EE:0F:40:4F","advertising":{},"rssi":-41}
{"id":"E6:D4:CD:29:E9:58","advertising":{},"rssi":-69}
Note that not all peripheral objects have a name property. We’ll need to accommodate this fact later
on.
Our application lists any device of interest, as determined by the isMyDevice function, in a table in the
device_list screen. Note currently, the isMyDevice function always returns true so that all devices that
are discovered are listed. Take a look at that function to be sure you’re clear on this point.
The showDiscoveredDevice function takes care of updating the UI with details of the discovered device.
Take a look at it if you’re interested in how this dynamic HTML update is achieved.
Now add the following code under the second TODO comment.
20
function scanFailure(reason) {
alert("Scan Failed: "+JSON.stringify(reason));
}
This function will be called if an error occurs whist scanning. A parameter containing information about
the nature of the failure will be included in the call to this function. To keep things simple, all we’ll do in
this situation is display an error dialogue.
Now initiate scanning for 5 seconds, using the Cordova ble API. Provide our success and failure functions
as arguments as shown below.
//TODO start scanning for devices for 5 seconds
ble.scan([], 5, onDeviceFound, scanFailure);
showMessage("Scanning....");
ble.scan is of course, one of the Cordova plugin’s API functions. Check the documentation for the
function to ensure you understand each of the 4 arguments. The only one which might not be
immediately understandable is the first one, which we’re not using.
showMessage is a function, provided in the starter code, which displays a message in the scanning
screen UI.
The fourth and final step of this part of the exercise requires us to add some code which will check
whether or not any devices were found and displayed in the device list. Add the following code to the
startScan() function:
//TODO check outcome of scanning after 5 seconds have elapsed using a timer
setTimeout(function() {
showMessage("");
if (!found_my_device) {
alert("Could not find your device");
}
}, 5000);
To understand this code, requires us to look at how the found_my_device variable is used. The starter
code initialises it to false when startScan is first entered. In the onDeviceFound function, we set it to true
if it’s selected by the isMyDevice function, which currently, all discovered devices are. setTimeout is a
standard JavaScript function and it sets up a delayed call to a specified function. In our case, we call that
function 5000ms later and as you can see, we check the found_my_device variable to see whether or
not it has been set to true, indicating that at least one device was found.
If the scanning process times out while you are considering granting permissions, you’ll see an alert box
with the message “Could not find your device”. If this happens, just click Scan again.
When you’ve successfully initiated scanning, you should see a list of all of the advertising Bluetooth LE
devices in range appear, including your peripheral BDSK device. It could look something like this:
22
Figure 14 - List of all devices found during scanning
4. Device Filtering
At this stage, our application scans for and lists *all* Bluetooth peripherals it finds. It would be much
better if it only displayed BDSK peripheral devices since these are the only devices our application will
work with. The isMyDevice JavaScript function will take care of this with a simple modification.
Towards the top of app.js we have the following constant defined:
app.device.ADV_NAME = 'BDSK'
This is the value which the peripheral includes in its Bluetooth advertising packets and next we’ll modify
the isMyDevice function to check for it.
Update isMyDevice so that it looks like this:
app.isMyDevice = function(device_name)
{
console.log("isMyDevice("+device_name+")");
//TODO implement device name matching
if (device_name == null || device_name == undefined) {
return false;
23
}
console.log('device name: ' + device_name);
return (device_name == app.device.ADV_NAME);
};
Notice that we allow for the possibility that the device to be checked is not advertising a name. If, on the
other hand, the advertisied name is “BDSK” then we’ve found our peripheral device and we return true
so that only this device is selected and displayed in the scan results list.
6. Summary of Exercise 1
And that is the first exercise done. To recap, in this exercise we performed these tasks:
1. Implemented Bluetooth scanning and listed all advertising Bluetooth peripherals found.
2. Improved scanning by filtering out all devices except the peripheral running the BDSK code from
the server lab of the Bluetooth LE Developer Study Guide.
24
Exercise 2 – Communicating with the peripheral
In the next part of this lab we will implement functions which allow the user to interact with the
peripheral over Bluetooth in various ways, as described in the functional requirements table above. The
user interface is already present in the starter project so that all we need to focus on is the Bluetooth-
related functionality.
1. Selecting a device
The device(s) listed on the scan results screen are in fact HTML buttons which have been dynamically
inserted into the UI. Each one has an onclick property and was created from a JavaScript string that
looks like this:
In all cases, selecting one of the listed devices triggers a call to the showMain function. The Bluetooth
device address is passed to showMain as an argument. Review this code and trace the path that device
addresses take, from having been discovered during scanning, to being passed through to showMain
when selected by the user.
When showMain executes, we store the selected device’s address in a variable:
selected_device_address = address;
25
Figure 15 - The main screen
Note that the selected device address has been displayed near the top of the UI.
The HTML which defines this screen is shown in Figure 16.
26
the state of the main UI screen, including the label on the Connect button and what happens when the
user clicks it.
As you can see from Figure 15, clicking the CONNECT button causes the toggleConnectionState()
function to be called. Update toggleConnectionState so that it matches the following code:
app.toggleConnectionState = function() {
console.log("toggleConnectionState("+selected_device_address+") : connected="+connected);
if (!connected) {
app.connectToDevice(selected_device_address);
} else {
// We’ll add this code later!
}
};
To start with, we’ll focus on the act of connecting to the peripheral. We’ll deal with disconnecting (when
the connected variable indicates we’re already connected), later.
Find the connectToDevice function in app.js. As the name suggests, we’ll implement code which
connects the smartphone to the peripheral here. But due to the nature of the API we’re using, we also
need to handle failed attempts to connect and both unexpected and explictly requested disconnects.
The API function which initiates connecting is the ble.connect function. It uses the familiar code pattern,
requiring arguments that include a function which handles the success case and one which handles
failures. The failure function is called if the attempt to connect to the BDSK device fails or if the
connection is dropped later on, either unexpectedly or deliberately.
app.connectToDevice = function(device_address)
{
console.log('connectToDevice: Connecting to '+device_address);
//TODO connect to the BDSK device and handle failures to connect and unexpected
disconnections
function onConnected(peripheral)
{
}
function onDisconnected(peripheral)
{
}
ble.connect(device_address, onConnected, onDisconnected);
};
27
That’s the basic, skeleton code in place. The peripheral parameter passed to the two functions is a JSON
object which contains information about the device we attempted to connect to.
5. onConnected
Now let’s complete the onConnected function, which is the function which gets called when we
successfully connect. Here’s what we need to do:
app.connectToDevice = function(device_address)
{
console.log('connectToDevice: Connecting to '+device_address);
//TODO connect to the BDSK device and handle failures to connect and unexpected
disconnections
function onConnected(peripheral)
{
console.log('connectToDevice: onConnected');
connected = true;
// check we have the required services
if (app.hasService(peripheral,app.device.IMMEDIATE_ALERT_SERVICE)
&& app.hasService(peripheral,app.device.LINK_LOSS_SERVICE)
&& app.hasService(peripheral,app.device.PROXIMITY_MONITORING_SERVICE)
&& app.hasService(peripheral,app.device.HEALTH_THERMOMETER_SERVICE)
) {
app.setControlsConnectedState();
showInfo("connected",0);
} else {
showInfo("ERROR: missing required GATT services",2);
ble.disconnect(
selected_device_address,
function() {
console.log("connectToDevice: disconnected OK");
showInfo("Disconnected");
},
function(error) {
console.log("connectToDevice: disconnect failed: "+error);
alert("Error: Failed to disconnect");
28
showInfo("Error: Failed to disconnect",2);
});
}
}
function onDisconnected(peripheral)
{
}
ble.connect(device_address, onConnected, onDisconnected);
};
Review the code we just added and make sure you understand it. The hasService function just searches
through an array of service UUIDs which is contained within the JSON peripheral object provided by the
ble API and returns true if a match is found. Our application requires the connected device to host all
three of the Link Loss, Immediate Alert and Proximity Monitoring services. Their UUIDs are defined
towards the top of the app.js file. The setControlsConnectedState sets UI controls like buttons into the
appropriate state now that we have a connection.
If the device does not have the required services, as you can see, we use the API ble.disconnect to
deliberately disconnect from it after displaying an error message.
6. onDisconnected
Now add the following code to the onDisconnected function.
app.connectToDevice = function(device_address)
{
console.log('connectToDevice: Connecting to '+device_address);
//TODO connect to the BDSK device and handle failures to connect and unexpected
disconnections
function onConnected(peripheral)
{
console.log('connectToDevice: onConnected');
connected = true;
// check we have the required services
if (app.hasService(peripheral,app.device.IMMEDIATE_ALERT_SERVICE)
&& app.hasService(peripheral,app.device.LINK_LOSS_SERVICE)
&& app.hasService(peripheral,app.device.PROXIMITY_MONITORING_SERVICE)
&& app.hasService(peripheral,app.device.HEALTH_THERMOMETER_SERVICE)
) {
app.setControlsConnectedState();
showInfo("connected",0);
} else {
showInfo("ERROR: missing required GATT services",2);
29
ble.disconnect(
selected_device_address,
function() {
console.log("connectToDevice: disconnected OK");
showInfo("Disconnected");
},
function(error) {
console.log("connectToDevice: disconnect failed: "+error);
alert("Error: Failed to disconnect");
showInfo("Error: Failed to disconnect",2);
});
}
}
function onDisconnected(peripheral)
{
console.log('connectToDevice: onDisconnected');
// called if ble.connect fails OR if an established connection drops
if (!connected) {
// we tried to connect and failed
console.log('connectToDevice: Error: Connection failed');
console.log(JSON.stringify(peripheral));
showInfo("Error: not connected",2);
alert("Error: could not connect to selected device");
} else {
// we were already connected and disconnection was unexpected
showInfo("Error: unexpectedly disconnected",2);
}
connected = false;
app.setControlsDisconnectedState();
}
ble.connect(device_address, onConnected, onDisconnected);
};
The onDisconnected function merely determines whether the event was expected or unexpected and
informs the user accordingly. It also calls a canned function to set the UI into an appropriate state for
when we’re not connected to the BDSK device.
We also need to update the toggleConnectionState code so that if we’re currently connected, executing
this function will cause the smartphone to disconnect from the peripheral. Update the function as
shown here:
app.toggleConnectionState = function() {
30
console.log("toggleConnectionState("+selected_device_address+") : connected="+connected);
if (!connected) {
app.connectToDevice(selected_device_address);
} else {
console.log("disconnecting");
ble.disconnect(
selected_device_address,
function() {
console.log("toggleConnectionState: disconnected OK");
showInfo("Disconnected");
connected = false;
app.setControlsDisconnectedState();
},
function(error) {
console.log("toggleConnectionState: disconnect failed: "+error);
alert("Failed to disconnect");
showInfo("Error: Failed to disconnect",2);
});
}
};
31
7. Disconnect when back button pressed
When the user exits the main screen, either using their smartphone’s navigation controls or by selecting
the Back button on our UI, we should disconnect from the peripheral, assuming we’re currently
connected.
Find the exitMain() function, which currently looks like this:
app.exitMain = function() {
app.showDeviceList();
};
Replace the code in the exitMain function so that it now looks like this:
app.exitMain = function() {
if (connected) {
showInfo("Disconnecting");
ble.disconnect(
selected_device_address,
function() {
console.log("disconnected OK");
app.showDeviceList();
},
function(error) {
console.log("disconnect failed: "+error);
alert("Failed to disconnect");
app.showDeviceList();
});
} else {
app.showDeviceList();
}
}
Review the new exitMain code. In all cases, we leave the main screen and show the device discovery
screen. If we’re currently connected, we disconnect.
32
o The peripheral LCD display or peripheral console (depending on the peripheral you’re
using) should indicate a connection has been established and the application UI should
enable various buttons.
2. Press DISCONNECT.
o The peripheral LCD / console should say “Disconnected” and most buttons on the
application UI should be disabled.
3. Press CONNECT again
o Per test result (1) above
4. Unplug your peripheral and wait approximately 20-30 seconds.
o The smartphone application should notice that the connection has dropped
unexpectedly and inform the user.
5. Plug the peripheral back in and press the Connect button on the UI
o Per test result (1) above
Note: if you have an LCD display connected to your peripheral or you’re using your device’s console for
output (see the server lab documentation), you’ll see it change in response to the peripheral being
connected to or disconnected from.
Note: if you’re wondering why it takes 20-30 seconds for your application to notice that the peripheral
has disconnected, this is due to a Bluetooth timeout parameter called the Supervisor Timeout which is
associated with the connection and determines how long the devices should wait to hear from the other
one at the Bluetooth link layer, before concluding the connection has dropped. Your smartphone is
setting the supervisor timeout when it connects to the peripheral and the APIs available to smartphone
developers do not allow them to modify the value used.
Find the establishCurrentAlertLevel() function in the app.js file and update it so that it looks like this:
app.establishCurrentAlertLevel = function() {
console.log("establishCurrentAlertLevel");
ble.read(selected_device_address, app.device.LINK_LOSS_SERVICE,
app.device.ALERT_LEVEL_CHARACTERISTIC,
function(data) {
console.log("read current link loss alert level OK");
var alert_level_data = new Uint8Array(data);
if (alert_level_data.length > 0) {
console.log("alert_level="+alert_level_data[0]);
33
alert_level = alert_level_data[0];
app.setAlertLevelSelected();
}
},
function(err) {
console.log("ERROR: reading current alert level: "+err);
});
}
The ble.read function is part of the Bluetooth LE API we’re using and it initiates the reading of a specified
characteristic. The arguments it requires are the address of the device we’re communicating with, the
UUID of the GATT service that contains the characteristic we want to read, the UUID of the charcteristic,
a function which receives the read data if the operation succeeds, and as usual a function to call in the
event of an error.
function(data) is the success function and data contains the value of the characteristic which was
obtained. We treat it as a byte array and take the first byte only, converting it into a standard JavaScript
variable. We store the value for future use and update the UI by calling a pre-written function called
setAlertLevelSelected which was provided in the starter project. This causes the LOW, MID or HIGH
button text to change colour to red, depending on whether the characteristic value was 0, 1 or 2
respectively.
We need to call establishCurrentAlertLevel after connecting and confirming that the remote device
contains the right GATT services. Find the onConnected function inside the connectToDevice function
and add the call to the if block which checks the GATT services which have been discovered:
34
10. Set the Alert Level
Next, we’ll add code which responds to the LOW, MID and HIGH buttons being pressed by writing a
corresponding value of 0, 1 or 2 to the Link Loss service’s Alert Level characteristic. Find the
setAlertLevel function in app.js and update it so that it looks like this:
app.setAlertLevel = function(level) {
console.log("setAlertLevel("+level+")");
var alert_level_bytes = [0x00];
alert_level_bytes[0] = level;
var alert_level_data = new Uint8Array(alert_level_bytes)
ble.write(
selected_device_address,
app.device.LINK_LOSS_SERVICE,
app.device.ALERT_LEVEL_CHARACTERISTIC,
alert_level_data.buffer,
function() {
console.log("written LL alert level OK");
alert_level = level;
app.setAlertLevelSelected();
},
function(e) {
console.log("ERROR: writing LL alert level: "+e);
});
};
The ble.write API is similar in form to the others we’ve seen so far. The device is identified in the first
argument and then a service UUID and characteristic UUID identify the characteristic to write to. The
fourth argument contains the data value to write, in the form of a byte array and the final two
arguments are a function for the positive case and a function for handling errors. Code prior to calling
ble.write is simply preparing the specified level value in the required format. Note that in the success
case, we call setAlertLevelSelected to update the LOW, MID or HIGH button so that the user can see that
it has been selected.
35
o The LOW button should become highlighted in red to indicate the current alert level has
been set to “LOW” (0). If you have the appropriate circuit connected to your peripheral
you should see the green LED flash by way of acknowledgement.
3. Go back and disconnect. Select and connect again.
o The LOW button should be highlighted red to indicate the current alert level read back
from the peripheral is “LOW” (0).
4. Select MID.
o The MID button should be highlighted in red to indicate the current alert level been set
to “MID” (1). If you have the appropriate circuit connected to your peripheral you
should see the yellow LED flash by way of acknowledgement.
5. Go back and disconnect. Select and connect again.
o The peripheral LEDs should all flash when you disconnect. This is the peripheral’s
responding to the link having been lost.
o The MID button should be highlighted red to indicate the current alert level read back
from the peripheral is “MID” (1).
6. Select HIGH.
o The HIGH button should be highlighted in red to indicate the current alert level read
back from the peripheral is “HIGH” (2). If you have the appropriate circuit connected to
your peripheral you should see the red LED flash by way of acknowledgement.
7. Go back and disconnect. Select and connect again.
o The peripheral LEDs should all flash and the buzzer should sound at the same time
when you disconnect. This is the peripheral’s responding to the link having been lost.
o The HIGH button should be highlighted red to indicate the current alert level read back
from the peripheral is “HIGH” (2).
app.startRssiPolling = function() {
console.log("startRssiPolling");
if (connected) {
rssi_timer = setInterval(
function() {
if (connected) {
36
ble.readRSSI(selected_device_address,
function(rssi) {
document.getElementById("rssi").innerHTML = "RSSI = "+rssi;
var rssi_value = parseInt(rssi);
if (rssi_value < -80) {
rectangle.style.backgroundColor = "#FF0000";
} else if (rssi_value < -50) {
rectangle.style.backgroundColor = "#FF8A01";
} else {
rectangle.style.backgroundColor = "#00FF00";
}
},
function(err) {
console.log('Error: unable to read RSSI',err);
showInfo("Error: unable to read RSSI",2);
clearInterval(rssi_timer);
})
}
},
1000);
}
};
As described, if we’re in a connection with the peripheral, this code establishes a timer which calls
ble.readRSSI every second. This is another of our Bluetooth API functions. It’s three arguments are the
address of the selected device, a success function which gets passed the RSSI value and an error
function which gets called if something goes wrong.
In the success value, we display the measured RSSI value and then we simply check it against three
thresholds (which were established through experimentation) and use this to designate the peripheral
as being nearby, at middle distance from the smartphone or at a long distance from it (but still in range).
We then set the UI proximity indicator rectangle to green, yellow or red accordingly.
Add a call to startRssiPolling in the onConnected function within the connectToDevice function.
function onConnected(peripheral)
{
console.log('connectToDevice: onConnected');
connected = true;
app.startRssiPolling();
// check we have the required services
if (app.hasService(peripheral,app.device.IMMEDIATE_ALERT_SERVICE)
&& app.hasService(peripheral,app.device.LINK_LOSS_SERVICE)
37
&& app.hasService(peripheral,app.device.PROXIMITY_MONITORING_SERVICE)
&& app.hasService(peripheral,app.device.HEALTH_THERMOMETER_SERVICE)
) {
When we disconnect, we need to stop the timer which polls the RSSI value. Update the
toggleConnectionState function as shown below:
app.toggleConnectionState = function() {
console.log("toggleConnectionState("+selected_device_address+") : connected="+connected);
if (!connected) {
app.connectToDevice(selected_device_address);
} else {
console.log("disconnecting");
clearInterval(rssi_timer);
document.getElementById("rssi").innerHTML = " ";
ble.disconnect(
selected_device_address,
function() {
console.log("toggleConnectionState: disconnected OK");
showInfo("Disconnected");
connected = false;
app.setControlsDisconnectedState();
},
function(error) {
console.log("toggleConnectionState: disconnect failed: "+error);
alert("Failed to disconnect");
showInfo("Error: Failed to disconnect",2);
});
}
};
38
13. Checkpoint: Test RSSI monitoring
Build and test your application. After connecting to the peripheral you should see RSSI values displayed
near the top of the screen and a coloured rectangle appear. Try moving nearer or further away from the
peripheral and watch the RSSI value change and occassionally, the rectangle change colour. The UI
should look something like Figure 16.
39
a noise if the alert level is greater than zero. If we write a value of zero then if there’s already an alert in
progress it should be silenced.
Find the makeNoise function in app.js and update it so that it looks like this:
app.makeNoise = function() {
console.log("makeNoise");
var alert_level_bytes = [0x00];
alert_level_bytes[0] = alert_level;
var alert_level_data = new Uint8Array(alert_level_bytes)
ble.writeWithoutResponse(
selected_device_address,
app.device.IMMEDIATE_ALERT_SERVICE,
app.device.ALERT_LEVEL_CHARACTERISTIC,
alert_level_data.buffer,
function() {
console.log("written IA alert level OK");
},
function(e) {
console.log("ERROR: writing IA alert level: "+e);
});
};
Note that here we used the ble.writeWithoutResponse API function rather than the ble.write function.
This is because the alert level characteristic when used within the Immediate Alert service supports the
GATT write without response procedure but not the write (with response) procedure.
40
var proximity_bytes = [0x00, 0x00];
proximity_bytes[0] = proximity_band;
proximity_bytes[1] = rssi;
var proximity_data = new Uint8Array(proximity_bytes)
ble.writeWithoutResponse(
selected_device_address,
app.device.PROXIMITY_MONITORING_SERVICE,
app.device.CLIENT_PROXIMITY_CHARACTERISTIC,
proximity_data.buffer,
function() {
console.log("written client proximity OK");
},
function(e) {
console.log("ERROR: writing client proximity: "+e);
});
};
We need to call this function every time we have a new RSSI reading, but only if the Share checkbox in
the UI has been selected.
Find the startRssiPolling function in app.js and make the changes highlighted below:
app.startRssiPolling = function() {
console.log("startRssiPolling");
if (connected) {
rssi_timer = setInterval(
function() {
if (connected) {
ble.readRSSI(selected_device_address,
function(rssi) {
document.getElementById("rssi").innerHTML = "RSSI = "+rssi;
proximity_band = 3;
var rssi_value = parseInt(rssi);
if (rssi_value < -80) {
rectangle.style.backgroundColor = "#FF0000";
} else if (rssi_value < -50) {
rectangle.style.backgroundColor = "#FF8A01";
proximity_band = 2;
} else {
rectangle.style.backgroundColor = "#00FF00";
proximity_band = 1;
}
41
if (sharing) {
app.shareProximityData(proximity_band, rssi_value);
}
},
function(err) {
console.log('Error: unable to read RSSI',err);
showInfo("Error: unable to read RSSI",2);
clearInterval(rssi_timer);
})
}
},
1000);
}
};
All we do here is set a variable called proximity_band to value of 1, 2 or 3, representing near, middle
distance or far away and then if the Share switch has been selected, we call the shareProximityData
function which will write the RSSI value and the proximity_band variable to the peripheral’s Client
Proximity characteristic.
One last thing we need to do is tell the peripheral to clear any proximity sharing information on its LCD /
console display and switch off LEDs when we first connect to it so that we can start again.
Update onConnected inside connectToDevice as shown:
app.connectToDevice = function(device_address)
{
console.log('connectToDevice: Connecting to '+device_address);
//TODO connect to the BDSK device and handle failures to connect and unexpected
disconnections
function onConnected(peripheral)
{
console.log('connectToDevice: onConnected');
connected = true;
// this will clear the peripheral LCD / console of proximity info and switch off all LEDs
app.shareProximityData(0,0);
app.startRssiPolling();
The peripheral has been programmed to reset the LCD / console and LED lights in response to the Client
Proximity characteristic having a value of 0x0000 written to it.
42
Test this feature by toggling the Share switch after connecting to the peripheral. With sharing enabled
you should see RSSI values displayed on the LCD / console connected to the peripheral. The LED of the
same colour as our UI’s RSSI rectangle should also be lit.
43
18. Link Loss
The penultimate part of this lab is to implement the link loss procedure. When our Bluetooth connection
is lost, we need to have the smartphone sound an alarm if the alert_level variable has a value of greater
than zero.
The audio.js file contains functions for controlling the playback of a wave file which contains a recording
of an alarm sound. Our next task is to update the onDisconnected function inside connectToDevice so
that if the Bluetooth connection was unexpectedly lost, we play the alrm sound for 10 seconds.
Update your code as shown below:
function onDisconnected(peripheral)
{
console.log('connectToDevice: onDisconnected');
// called if ble.connect fails OR if an established connection drops
if (!connected) {
// we tried to connect and failed
console.log('connectToDevice: Error: Connection failed');
console.log(JSON.stringify(peripheral));
showInfo("Error: not connected",2);
alert("Error: could not connect to selected device");
} else {
// we were already connected and disconnection was unexpected
showInfo("Error: unexpectedly disconnected",2);
audio.play(alarm_sound_path);
setTimeout(function() {
audio.stop();
}, 10000);
}
connected = false;
app.setControlsDisconnectedState();
}
In the event that we experience an unexpected disconnection, we call audio.play to start the alarm
sound and set up a JavaScript timer to call audio.stop 10 seconds later to stop it.
We also need to stop the alarm if it is sounding when we (re)connect to the peripheral. Update the
onConnected function as shown:
function onConnected(peripheral)
{
console.log('connectToDevice: onConnected');
connected = true;
44
audio.stop();
// this will clear the peripheral LCD / console of proximity info and switch off all LEDs
app.shareProximityData(0,0);
app.startRssiPolling();
app.setTemperatureMonitoring = function() {
console.log("setTemperatureMonitoring");
monitoring_temperature = cb_temperature.checked;
console.log("monitoring_temperature="+monitoring_temperature);
if (monitoring_temperature) {
app.startIndications();
} else {
app.stopIndications();
}
};
As you can see, we’re calling one of two functions to either start or stop temperature measurement
indications, according to the state of the UI switch. Let’s add those functions now:
app.stopIndications = function()
{
console.log("stopping indications");
ble.stopNotification(
selected_device_address,
app.device.HEALTH_THERMOMETER_SERVICE,
app.device.TEMPERATURE_MEASUREMENT_CHARACTERISTIC);
document.getElementById('temperature').innerHTML = 'not available';
};
app.startIndications = function () {
45
console.log('Starting indications');
ble.startNotification(selected_device_address,
app.device.HEALTH_THERMOMETER_SERVICE,
app.device.TEMPERATURE_MEASUREMENT_CHARACTERISTIC,
function (buffer) {
monitoring_temperature = 1;
var data = new Uint8Array(buffer);
console.log("data="+data.toString());
var signed_data = new Int8Array(buffer);
var units = "C";
if (data[0] == 1) {
units = "F";
}
// 4 bytes required for Int32 conversion. Also, make big endian while we're at it.
var mantissa_bytes = new Uint8Array([0x00,0x00,0x00,0x00]);
mantissa_bytes[1] = data[3];
mantissa_bytes[2] = data[2];
mantissa_bytes[3] = data[1];
// propogate sign bit of most significant byte of the 3 mantissa bytes
if (mantissa_bytes[1] & 0x80 == 0x80) {
// sign bit is set so....
mantissa_bytes[0] = 0xff
}
46
monitoring_temperature = 0;
});
};
The API we’re using makes no apparent distinction between indications (which require a response from
the client) and notifications (which do not), so don’t be confused by the fact that the API calls are to
functions startNotifications and stopNotifications.
When starting indications, we supply the selected device’s address and the service and characteristic
UUIDs for the characteristic whose values we want transmitted as indications. We also supply two
functions, one for the success case and one for the error case. The success function is called every time
we receive an indication from the peripheral device and we receive the value as an ArrayBuffer. Using
JavaScripts typed array functions, we convert the ArrayBuffer into both a signed octet version and an
unsigned version. This is because we’re receiving 5 octets in total, all of which are unsigned except for
one and this is just an easy way to ensure we have access to values with or without their sign, as we
require.
The first octet tells us whether the data is in degrees Celcius (0) or Fahrenheit (1). The following 3 octets
comprise the mantissa in a floating point number and the final octet is the exponent. Using this data we
calculate a temperature in the appropriate units and insert it into our main web page for display.
Note that we also maintain a flag called monitoring_temperature. A value of 1 means that indications
are currently enabled. We’ll use that to unsubscribe from them when the user decides to go back to the
device discovery screen or disconnect from the peripheral.
Update the exitMain function as shown:
app.exitMain = function() {
if (connected) {
if (monitoring_temperature == 1) {
app.stopIndications();
}
showInfo("Disconnecting");
ble.disconnect(
selected_device_address,
function() {
console.log("disconnected OK");
app.showDeviceList();
},
function(error) {
47
console.log("disconnect failed: "+error);
alert("Failed to disconnect");
app.showDeviceList();
});
} else {
app.showDeviceList();
}
}
And finally, update toggleConnectionState so that if the user opts to disconnect, we first unsubscribe
from temperature indications if they’re active.
app.toggleConnectionState = function () {
Scan Button finds your peripheral BDSK device which should have
the name “BDSK”.
Select any of the Low, Mid or High buttons Write 0, 1 or 2 to the Link Loss service Alert Level
characteristic in the peripheral. The corresponding LED
(0=green, 1=yellow, 2=red) on your board should flash 4
times as an acknowledgement.
48
Make a Noise The Alert Level characteristic of the Immediate Alert
service should be written to using the value last selected
by pressing one of Low, Mid or High. If the alert level is
1 or 2, the board should respond by beeping 5 times and
flashing all three LEDs in unison. If 0 then the LEDs
should flash but the buzzer should be silent.
Link Lost / Disconnected The phone and board should make a noise for an
extended period of time and all LEDs should flash.
Well done! By following this hands-on lab you have created an application that scans for Bluetooth
peripheral devices, can establish a connection to a selected device, examines the GATT services it
supports, reads and writes characteristic values, monitors the RSSI value and can enable, disable and
process temperature measurement indications. What’s more, you can build a version which runs on
Android and a version which runs on iOS all from the same source code!
49