Build connected devices using Bluetooth Low Energy (BLE) APIs

We live in an age of devices and apps and “things.” Things is a term that is used to describe a material object with connectivity. When we think of connectivity, we think of devices which are connected to the Internet, hence the term Internet of Things, or IoT.

Device manufactures face two key challenges when bringing a product to market today. The first challenge is that it is not easy, or cost effective, to add a secure internet connection to a device. The second challenge, in an age where every user is accustomed to highly interactive, engaging touch-screen experiences, deploying a device with a two-line display and a couple of physical buttons simply does not get a passing grade.

This tutorial demonstrates the basics of connecting things using the Bluetooth Low Energy protocol, or simply, BLE. BLE is the latest iteration of the Bluetooth wireless application protocol family. Bluetooth was introduced as a “replacement for wires” technology, essentially as a wireless communications port. It is often still referred to as the “Serial Port Profile,” or RFCOMM. As Bluetooth gained traction, IrDA interfaces declined, because Bluetooth’s range and lack of line-of-sight capabilities were welcome freedoms.

As Bluetooth has matured, other profiles have become ubiquitous in the marketplace, most notably with wireless hands-free devices and speakers. One challenge of traditional Bluetooth devices is the need to pair devices manually. At best, this becomes a not-really-secure experience of entering 0000 or 1234 into a popup dialog; or, at worst, it is a frustrating experience of devices never pairing or, even worse, losing their relationship in the middle of using the device. BLE addresses a number of these issues by essentially rewriting the rules of engagement, and, in doing so, expanding the types of applications available for devices.

Covering the details of BLE would require an entire book. In fact, many books are on the market along with the various protocol and working group specifications. The aim of this tutorial is to convey a working knowledge of using BLE on an Android device. Because a BLE connection requires a counter-party (that is the “other” device), we spend some time with a simple “device” that also uses the BLE protocol, but from the opposite perspective. This process of looking through both ends of the telescope will hopefully convey enough of a starting point for your own applications, and perhaps even enable you to create a dynamic user interface for a device of your own design. Our Android application talks to an Arduino-based device which actuates a garage door opener. Yes, we are creating a very expensive garage door opener app!

The Android application uses a hybrid mobile application architecture – a combination of native application code with a web-based user interface.

This tutorial introduces the Android Bluetooth Low Energy (BLE) capabilities embedded in the Android Software Developer Kit, which is accessible in the Android Studio environment. BLE permits devices to communicate wirelessly, without line of sight, with a range of +/- 10 meters. Unlike previous iterations of Bluetooth technology, BLE does not require pairing in order to exchange data. This opens up new application areas such as beacons that are particularly useful in retail and material handling applications.

The application that we build has an Android phone that connects to an Arduino-based circuit that actuates a slightly modified commercial garage door opener by using an opto-isolated solid state relay. Lots of fancy words, but in reality, it is a simple application, once we get our head around how BLE works. The hybrid web user interface and even the garage door remote control interface are really window-dressing on the heavy lifting of this tutorial, which is an exploration of how to get BLE to work for you.

We begin the tutorial with a high level look at the application, an Android device that communicates via BLE to an Arduino-based circuit. Then, we introduce aspects of BLE by introducing code snippets from both the Android side, which is in the “central” role, and from the Arduino side, which is in the “peripheral” role. A look ahead to the completed project’s source files provides a roadmap for the experience that is constructed in the application. Then, in a step by step manner, you will construct this application yourself. Both the Java Android application and the C++ Arduino application are explained.

Before you begin

You should be comfortable constructing Android applications with the Android SDK as well as have a basic familiarity with the C/C++ programming language to get the most out of this tutorial. The Arduino environment utilizes a flavor of C. If you’ve never built an Android app before, you can work through my other tutorial, “Developing your first Android application.”

If you have the ambition to build this application, the parts are readily available on the internet for less than $50. After you complete this tutorial, you will have learned how to explore BLE devices, generate simple hybrid mobile applications, and even program an Arduino device. You will want to be very comfortable interacting with the LogCat in Android Studio to trace your way through the various interactions between the Android and Arduino devices.

As developers, it is our job to build security in from the start. The traditional view of information security could be wrapped up in the acronym, CIA, which stands for Confidentiality, Integrity, and Availability. For the BLE and Arduino portions of this tutorial, we will highlight a few places where you will want to consider adding your own security aspects to keep your data/device/home Confidential, having Integrity, and certainly Available. However, in a day where devices can connect with the physical world, we need to introduce a fourth dimension to our long standing CIA triad. The new-comer is, “S” for Safety.

WARNING: If you choose to build this application, including constructing the circuit, you have the opportunity to remotely control a device while not actually having eyes-on the physical space. This can lead to injuries or death, particularly with small children in the area. I implore you to take every precaution when working with this or any other application with real-world implications.

Prerequisites

The code samples for this tutorial were created on a MacBook Pro with Android Studio 3.5.1 and Arduino IDE version 1.8.10. The Android application is targeting version 21 of the Android SDK, which is the version of the SDK where BLE was introduced.

Having a commercial BLE application, such as LightBlue Explorer from PunchThrough, to explore the devices in your world is a great first step in getting started with BLE development. If you do not already have one of these applications, take a few minutes to install one and do some exploring of the nearby devices. See if you can identify your laptop, smartphone, headphones, car, or other Bluetooth devices in or around your home or office.

To build this mobile and IoT solution for yourself, you need the following hardware and software.

Hardware

  • Arduino Nano 33 BLE Single Board Computer, which is the physical board with an onboard BLE chip. (You can read more about IoT device hardware in this developer hardware guide.)
  • LiftMaster Garage Door Opener. This actually opens my garage door. Feel free to substitute an LED or even a multi-meter with a continuity tester, which are probably both safer experiences!
  • Toshiba TLP-222A Solid State Relay, which connects the Arduino to the garage door remote.
  • Hobbyist breadboard and hook up wire, which is used to hold and connect the few components of our circuit.

Software

  • Android Studio, which is the primary code editor, Java Compiler, and debugging environment.
  • Android SDK. In particular, you will become quite familiar with the Android SDK help documentation for BLE.
  • Android phone or tablet. I am using a low-cost Samsung device.
  • Arduino IDE, which is the Arduino code editor and board programmer.
  • ArduinoBLE Library, which is the code library to simplify working with BLE. This is installed via the Arduino IDE, so no need to take heroic steps. You’ll likely become familiar with its help doc, too.

A superficial, high-level description of BLE

To build BLE applications, you need to know how BLE works.

I can explain how BLE works in a couple of different ways. One way is to provide a very detailed, specification-focused and technically-accurate version of the terms, complete with tables and state-machine diagrams, which are all useful. Or, I can stick to the elements that matter for getting up and running with an application talking to a device, with just enough scaffolding to understand and put things to work. In this tutorial, we are opting for the latter approach. Once you have a little momentum with working with BLE, the myriad of available books will make more sense. With that disclaimer behind us, let’s dive in to explaining the way BLE works.

There are two types of BLE devices: the “Central” device and the “Peripheral” device. While most devices can play either role, if you are building an Android application to connect to a physical device using BLE, the Android device is going to play the Central role. Similarly, if you’re designing a microcontroller-based device, similar to what we’ve built on Arduino, you’re constructing a Peripheral device.

The Peripheral device defines a collection of services, characteristics, and descriptors:

  • Services are required in order to be of interest — a device without at least one service is of no use. A service might look like a heart rate monitor, a thermometer, a generic sensor, or even, as in our case, an overly-complicated garage door opener. Services have characteristics.

  • Characteristics are similar to “member variables” in an object-oriented language in that they hold data and they represent values. The BLE space is crowded with fitness gear and headphones. Representative characteristics include things like heart rate (beats per minute) or volume. Characteristics can be Read Only, Write Only, Read/Write, and so on. Writing a new value to a characteristic is somewhat analogous to invoking a “setter” in an object-oriented language as the method will not only update the value, but has also the opportunity to take action upon that value being updated.

  • Descriptors provide additional information about a characteristic.

You can get lost in the hierarchy of attributes in BLE. Don’t let yourself get overwhelmed by it.

Every BLE device, service, or characteristic has a universally unique identifier, or UUID. A UUID is a 128-bit number. The lowest 96 bits are all the same and we are only concerned with the upper 32 bits. And, for the most part, the highest 16 bits are zero. So, generally speaking, that leaves us with 16 bits that are of interest.

Here is an example of a BLE UUID:

0000fa01-0000-1000-8000-00805f9b34fb]

The only portion that we really care about in this UUID is the fa01. That is the portion which identifies this device. Figure 1 shows our device being found in a scan by a commercial BLE application utility, LightBlue Explorer from PunchThrough.

Figure 1. LightBlue Explorer application from PunchThrough permits interrogating devices.

LightBlue Explorer application from PunchThrough permits interrogating devices.

BLE allows for device manufacturers to define any number of services, characteristics, and descriptors, but there are some pre-allocated values for devices and characteristics that are used to contain information that all devices need to provide, such as name or appearance. Figure 2 shows some of the characteristics of our Garage Door Opener device.

Figure 2. Characteristics of our device as discovered in LightBlue Explorer

Characteristics of our device as discovered in LightBlue Explorer

When constructing a central application, there are three high-level steps:

  • Find peripheral devices, and filter down to the device and service that you’re interested in. (See Figure 1.)
  • Read and write characteristics (See Figure 2).
  • Disconnect when desired

This tutorial demonstrates in varying levels of detail the code and functionality for both the central device (Android/Java) and the peripheral device (Arduino/C++).

Before diving into code, let’s take a moment to view the application in action.

The application in action

Using the application is as simple as loading it and selecting one of two buttons. The button on the left, which says “Interrogate Device,” triggers the application to find the BLE peripheral device and do some research on what services, characteristics, and so on are available.

The button on the right, which says “Toggle the Door,” triggers the application to find the device and then write to a specific characteristic. When the appropriate value is written to this characteristic, the peripheral device interprets the newly written value and selectively takes action.

Figure 3. The “Toggle the Door” action from the Android application’s perspective.

The "Toggle the Door" action from the Android application's perspective.

Figure 4 shows a portion of the LogCat as the Android application (the central device application) steps through the process of searching, finding, interrogating, communicating, and disconnecting. The code is reviewed in detail later in this tutorial.

Figure 4. LogCat from the Android device

LogCat from the Android device

Figure 5 shows a log of the activity from the Arduino side (the peripheral device). We are getting a little ahead of ourselves, but for now, know that writing a value of “0x55” (85 decimal) to characteristic “2102” is the step that is required to trigger the door to open.

Figure 5. Arduino log

The Arduino log

Steps

Step 1. Set up the hardware

Figure 6 shows our hardware setup.

Figure 6. Circuit connected to Garage Door Remote

Circuit connected to Garage Door Remote

The Garage Door Remote is connected via the orange and green wires to the output side of the relay.

When a value of “0x55” is written to the “2102” characteristic, GPIO Pin 10 is taken to 3.3 volts, permitting the input side of the relay to be triggered, since they are connected via the yellow wire.

The input side of the relay is an LED. (For the more sophisticated reader, yes, there should be a current limiting resistor to protect the input side of the relay.)

The orange and green wires are connected on one end to Pins 7 and 8 of the relay and on the other end to the printed circuit board (PCB) where a momentary push button switch used to be attached before I removed it by using a soldering iron.

The relay integrated circuit (IC) we are using actually contains two separate relay circuits. We are using the lower relay, as shown in Figure 7.

Figure 7. Solid State Relay. Toshiba TLP222A

Solid State Relay. Toshiba TLP222A

Pin 4 of the relay is connected to the ground rail, and Pin 3 is connected to Pin 10 on the Arduino. Normally, Pin 3 is LOW, which is equivalent to Pin 4’s voltage level. Pin 4 is LOW because it is tied to the ground rail via the brown wire. Therefore, no current flows through the LED and the relay is not actuated. The relay is a Normally Open (NO) device, so the contacts at pins 7 and 8 are not connected until the relay is activated. When Pin 3 of the relay is taken HIGH, current flows through the LED, the relay closes, and the remote control is triggered.

Step 2. Build the Arduino BLE peripheral application

Now it’s time to dig into some code. We’ll look at the peripheral code on the Arduino first because it helps to see the way in which the device-side is setup, which will bring more clarity once we circle back to the central Java code on the Android device.

Review the peripheral code

If you have not used the Arduino platform before, there are essentially two elements to a program: a setup routine and a loop routine. The setup routine is called once when the device boots, and then the loop routine is called for the main processing of the application. There can be other functions, of course, but those are the required functions. In this section, we will walk through the Arduino project. The source code is available in github.

Listing 1 shows the header required for this project along with the main BLE definitions and a couple of variables, as we are using the ArduinoBLE Library. Next, we define a number of elements required including a single BLEService referenced via a variable named garageDoorService with a UUID of FA01.

BLE libraries and examples are pretty casual with UUIDs, which are 16 bit values. Our example has a value of FA01, which actually means a hexadecimal value of 0xFA01. This is important because we could have just as easily assigned the UUID to be 1234. It is not immediately clear that it is a hex value. FA01 was entirely arbitrary, but I had to select something, so I used my initials plus 01.

Listing 1. Header portion of Peripheral code
#include <ArduinoBLE.h>
// create a service to expose our service to BLE Central Devices
BLEService garageDoorService("FA01");
BLEUnsignedCharCharacteristic buttonCharacteristic("2102", BLERead | BLEWrite | BLENotify);
BLEUnsignedCharCharacteristic batteryLevelChar("2101", BLERead | BLENotify);
BLEUnsignedCharCharacteristic scaledValueChar("2103", BLERead | BLENotify);
BLEDescriptor mydescriptor1("2103","ABC1");
BLEDescriptor mydescriptor2("2103","ABC2");
BLEDescriptor mydescriptor3("2103","ABC3");

byte buttonValue = 0x00;
int commandIterations = 0;
#define  BUTTONSWITCHPIN 10

After creating the service, we create a number of characteristics. Only the buttonCharacteristic (2102) is used to actually operate this application. The others were included as a learning exercise for you to expand on as desired. As you run the application and choose the “Interrogate” path, these other characteristics and descriptors come into play.

We define a single byte variable to hold the current value of buttonCharacteristic. An integer tracks the number of times the button is pressed, and we round out this section with a preprocessor defined for the General Purpose Input Output (GPIO) Pin assignment for controlling the switch. It is a best practice to create a named value such as this to make it easier to both read code over time and to make changes. You don’t want a random integer value in your application when you can give it a descriptive name!

In the Setup function shown in Listing 2, we do all of the necessary housekeeping to establish our peripheral BLE device.

Listing 2. Setup routine
void setup() {
  Serial.begin(9600);
  while (!Serial);
  pinMode(BUTTONSWITCHPIN,OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
  if (!BLE.begin())
  {
    Serial.println("starting BLE failed!");
    while (1);
  }

  String address = BLE.address();
  Serial.println("Our address is [" + address + "]");

  BLE.setDeviceName("IBM Garage Opener");      // this sets Characteristic 0x2a00 of Service 0x1800
                                               // Service 0x1800 is the Generic Access Profile
                                               // Characteristic 0x2a00 is the Device Name
                                               // Characteristic 0x2a01 is the "Appearance"
  BLE.setAppearance(384);                      // BLE_APPEARANCE_GENERIC_REMOTE_CONTROL

  BLE.setLocalName("BLE Garage Opener");       // this sets the local name for the advertising data

  // tell the world about us
  BLE.setAdvertisedService(garageDoorService);
  garageDoorService.addCharacteristic(batteryLevelChar);
  garageDoorService.addCharacteristic(buttonCharacteristic);
  scaledValueChar.addDescriptor(mydescriptor1);
  scaledValueChar.addDescriptor(mydescriptor2);
  scaledValueChar.addDescriptor(mydescriptor3);
  garageDoorService.addCharacteristic(scaledValueChar);
  BLE.addService(garageDoorService);

  buttonCharacteristic.writeValue(buttonValue);      // start with a zero

  // advertise to the world so we can be found
  BLE.advertise();

  Serial.println("Bluetooth device active, waiting for connections...");

  // register new connection handler
  BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
  // register disconnect handler
  BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);

  // handle Characteristic Written Handler
  buttonCharacteristic.setEventHandler(BLEWritten,switchCharacteristicWritten);
}

Some items of note in the setup function code:

  • We wire up various characteristics and descriptors.
  • We set an Advertise value. This tells central devices what we want to be seen as a Generic Remote Control (384). You can review the Nordic Semiconductor documentation page for a list of commonly used BLE appearance values.
  • We register event handlers for connect, disconnect, and characteristic written notifications. We want to handle each of these distinct events.

The main application loop is shown in Listing 3.

Listing 3. Main loop
void loop()
{
  BLEDevice central = BLE.central();

  if (central)
  {
    while (central.connected()) {
          int battery = analogRead(A0);
          int batteryLevel = map(battery, 0, 1023, 0, 100);
          delay(200);
    }
  }
}

The main loop code looks to see if we have a central device connected. If so, we go into an inner loop for the duration of the time that the central device remains connected. We read a couple of values and perform a short sleep operation of 200 milliseconds. During this housekeeping loop, a real device would perform other operations. Although like many microcontroller applications, the main loop is somewhat boring, with the more interesting functionality residing in the event handlers. On other microcontroller platforms, those event handlers are known as an interrupt service routine, or ISR.

Listing 4 shows the Connect and Disconnect event handlers. During these events, we perform some basic housekeeping including writing out to the serial debug monitor and toggling the LED indicator and relay to their appropriate values based on the state of the connection.

Listing 4. Arduino BLE Peripheral Connect and Disconnect Handlers
void blePeripheralConnectHandler(BLEDevice central) {
  // central connected event handler
  Serial.print("Connected event, central: ");
  Serial.println(central.address());
  digitalWrite(LED_BUILTIN, HIGH);    // indicate that we have a connection
  digitalWrite(BUTTONSWITCHPIN,LOW);          //  make sure our button is NOT pressed
}

void blePeripheralDisconnectHandler(BLEDevice central) {
  // central disconnected event handler
  Serial.print("Disconnected event, central: ");
  Serial.println(central.address());
  digitalWrite(LED_BUILTIN, LOW);     // indicate that we no longer have a connection
  digitalWrite(BUTTONSWITCHPIN,LOW);          //  make sure our button is NOT pressed
}

Let’s wrap up this review of the peripheral code by looking at the event handler that interacts with the remote control, shown in Listing 5. This function is called anytime the central device writes a value to the peripheral device. Keep in mind that a peripheral device could have numerous characteristics. Regardless of the characteristic that was changed, this same function is invoked. For our purposes, we check the UUID of the characteristic, and if it matches our desired value of “2102”, we further examine the new value. If the new value is “0x55”, we write a logic HIGH to our output pin, GPIO number 10, or more generally, BUTTONSWITCHPIN. We wait half a second, and then set the value LOW again. This is a reasonable approximation of what it is like for a human to click the door opener button.

Listing 5 Arduino BLE Peripheral Button Press Handler
void switchCharacteristicWritten(BLEDevice central, BLECharacteristic characteristic) {
  // central wrote new value to characteristic, see if care, etc.
  Serial.print("Characteristic event, written: ");
  Serial.println(characteristic.uuid());
  // see if this is the Button Characteristic
  if (characteristic.uuid() =="2102")
  {
    characteristic.readValue(buttonValue);
    Serial.print("new value ");
    Serial.println(buttonValue);
    if (buttonValue == 0x55)
    {
      digitalWrite(BUTTONSWITCHPIN,HIGH);
      delay(500);
      commandIterations++;
    }
    digitalWrite(BUTTONSWITCHPIN,LOW);
  }
}

The next step discusses the necessary steps for building and programming the Arduino application.

Build the Arduino application

Building applications in Arduino is straight forward. The Arduino platform is a great place to learn embedded development for the student or hobbyist, and it also serves as a fast and effective prototyping environment for the professional engineer.

If you do not already have the Arduino software installed, you can download the installation package from https://www.arduino.cc/en/Main/Software. Once installed, launch the the Arduino IDE.

When the Arduino IDE starts, it will create a new, empty application, or it will re-open the most recent application you were working with. At this point, go ahead and create a new project via the File > New menu. This creates a single new file with an .ino extension. Note that an Arduino project is referred to as a sketch.

If you haven’t already done so, you need to access the peripheral code from my github repository so that you can easily copy and paste the code into your newly created project.

After you’ve copied the code into your project (your sketch), select Add Library, and select ArduinoBLE. This should be available in your Arduino environment. If not, you may need to add it. Figure 8 shows the library installed under the Library Manager.

Figure 8. ArduinoBLE library is installed

ArduinoBLE library is installed

Under the Sketch menu, select Compile.

Plug in the Arduino Nano 33 BLE board to an available USB port on your development computer. Then, under the Tools>Port menu, select the board that you just plugged in.

Under the Sketch menu, select Upload. Note that this will typically compile your sketch and then proceed to uploading the compiled application code to your board. As you start making iterative changes to your sketch, you can skip the separate compile step and just use the upload step.

Once the sketch is running on your board, you will want to monitor the activity. If you noticed in the code snippets, there are a number of statements which use the “Serial” object. This is the debugging mechanism for Arduino projects. Anything written with the Serial.println() function shows up in the Serial Monitor under the Tools menu, as shown in Figure 9.

Figure 9. Enabling the Serial Monitor

Enabling the Serial Monitor

With this discussion of the BLE Peripheral behind us, let’s move on to the Android BLE central application.

Step 3. Build the Android BLE central application

Our Android application takes on the BLE central role – meaning that it will initiate communication with the BLE peripheral device. What we need the application to do is essentially:

  • Find our target BLE peripheral.
  • Connect to the BLE peripheral.
  • Initiate the command to toggle the garage door switch. As introduced previously, this action is initiated by writing a value of “0x55” to a specific characteristic, namely the characteristic with a UUID of “2102”.

In addition to taking this one action, we want our application to help us learn about BLE, so we have a second action called “Interrogate Device.” Both the “Interrogate Device” and the “Toggle Door” actions are initiated by button presses in the Android application. The two buttons are visible in the bottom of the application’s UI.

Build the Android application

Open Android Studio, and click File > New Project. Again, if you’ve never built an Android app before, you can work through my other tutorial, “Developing your first Android application.”

The main files in our Android application include these files:

  • MainActivity.java, which extends the Android Activity class for user interface and application logic.
  • activity_main.xml, which is the home page of the application user interface. This holds a single widget – WebView.
  • AndroidManifest.xml, which is the deployment descriptor for the Android application. While reviewing this file, note the specific permissions required for BLE. Because this application accesses the BLE capabilities of your Android device, these permissions are required in order for the application to work properly.
  • index.html, which is the user interface for our Android application.
  • BLEQueueItem.java, which is the queue to hold tasks. Bluetooth GATT queries cannot be overlapped, therefore we implement a queue to hold tasks. Each element of the queue is an instance of BLEQueueItem.

The Android application has a single Activity, named MainActivity, which houses the user interface. This application is a hybrid web and native application. The UI is implemented via a WebView control that is housed in the activity. The BLE code is implemented on the native side of the application.

Figure 10 shows the Android application’s project window in Android Studio.

Figure 10. Android Studio project window

Android Studio project window

Review the central application code

Let’s take a tour through the code for this Android application. You can download the source code from my github repository.

Listing 6 shows the user interface layout file used by the native application to represent the UI and Listing 7 shows the index.html file which is what users see and interact with. The index.html file is housed in the WebView control defined in Listing 6 and connected in the onCreate method of MainActivity.

Listing 6 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/browser"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Building applications in HTML, Javascript, and CSS is a friendly place to start for many developers new to mobile applications. The appeal of building an application with an embedded WebView control alongside traditional Java classes is that we enjoy the full benefits of both web tools and straight-up Android SDK interaction. We don’t need to wait for a tool vendor to implement the latest wrappers around the SDK. There are strategies for more complex hybrid applications, but for our purposes, we simply put all required elements into a single file which is stored under the assets folder of our Android application. Note that a few images are used and they are stored in the same folder.

Listing 7 The index.html
<html>
<head>
    <title>BLE Dynamic UI</title>
    <script
            src="https://code.jquery.com/jquery-3.3.1.min.js"
            integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
            crossorigin="anonymous"></script>
</head>
<script language="JavaScript">
function startup() {
    try {
        window.bleui.Info("Starting up....");
    } catch (ee) {

    }
}
function go() {
    document.getElementById('arduino').class='notconnected';
    window.bleui.interrogate();
}
function goopen() {
    document.getElementById('arduino').class='notconnected';
    window.bleui.toggleDoor();
}
function setClassOnArduino(c) {
    document.getElementById('arduino').className = c;
}
function setActuating(c) {
    document.getElementById('toshiba').className = c;
    document.getElementById('liftmaster').className = c;
}
function setStatus(s) {
    document.getElementById('status').innerHTML = s;

}
</script>
<style>
.notconnected {
    border: 5px solid red;
}
.searching {
    border: 5px dotted yellow;
}
.discovering {
    border: 5px dashed green;
}
.communicating {
    border: 5px solid green;
}
</style>
<body >
<center>
    <h3>Expensive Garage Door Opener</h3>
    <p>This demonstrates using an Arduino-powered BLE board</p>
    <p id="status"></p>
    <img id="arduino" style="width:35%;" src="arduino33.jpg"><br>
    <p>energizing a small opto-isolated solid state relay (Toshiba TLP222A)</p>
    <img id="toshiba" style="width:35%;" src="toshibarelay.jpg"><br>
    <p>To actuate to a (slightly modified) commercial garage door opener</p>
    <img id="liftmaster" style="width:35%;" src="garagedooropener.jpg"><br>
    <button onclick="go();">Interrogate Device</button>
    <button onclick="goopen();">Toggle the Door</button>
</center>
</body>
</html>

Looking at the code in Listing 7, note the following:

  • HTML is used for the primary user interface including the use of inline styles, click handlers, and so on.
  • The Style section assists with showing the status of the connection through the lifecycle of the applications functionality
  • The Script section contains a handful of helper functions
  • Invocations of window.bleui.* functions. These functions are essentially “backed” by the Java code, as explained below.

Listing 8 shows the AndroidManifest.xml file that highlights the permissions required for a BLE application.

Listing 8. Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.navitend.ble1">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

</manifest>

The major code elements of the application are listed in the following table:

Listing number Description Comments
9 Imports Note all of the BLE related classes!
10 Class level constants and variables Class level variables and constants.
11 onCreate method of MainActivity Sets up the necessary User Interface elements for the Android application.
12 WebViewClient and BLEUIHandler These handlers react to events triggered by the user and by device interactions.
13 BLEFoundDevice Handle BLE scanning events.
14 BLERemoteDevice Connect to remote device.
15 BLEQueueItem Handle interactions with remote device.
Listing 9. Required imports
package com.navitend.ble1;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.*;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelUuid;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.webkit.WebSettings;
import android.webkit.WebChromeClient;
import android.webkit.WebViewClient;

import java.util.LinkedList;
import java.util.Queue;
import java.util.List;
import java.util.UUID;

Our single Activity, MainActivity, wires up the application including a WebView/HTML/CSS/JavaScript user interface. Remember, our larger ambition is to explore building user interfaces for embedded devices. To this end, the UI aspects could be ported over to another platform such as iOS, permitting a common device end user experience. No code would need to change on the embedded device!

In Listing 10 we define a browser object for the user interface, a Handler for coordinating UI updates in a thread-safe manner, and a BluetoothAdapter for interacting with the Bluetooth stack.

Listing 10. Class level variables and constants defined for state transitions
public class MainActivity extends AppCompatActivity {
    private final String tag = "ble1";
    private WebView browser = null;
    private Handler mHandler = null;
    private int howmany = 0;
    private BluetoothAdapter bluetoothAdapter;

    private final int NOTCONNECTED = 0;
    private final int SEARCHING = 1;
    private final int FOUND = 2;
    private final int CONNECTED = 3;
    private final int DISCOVERING = 4;
    private final int COMMUNICATING = 5;
    private final int TOGGLEDOOR = 6;
    private final int DISCONNECTING = 7;
    private final int INTERROGATE = 8;

Listing 11 shows the onCreate method of MainActivity. Handling UI updates across the BLE callbacks requires the use of a Handler class to share messages. This approach permits the UI updates to happen in a thread-safe manner. Note the frequent calls to loadUrl with a javascript instruction. We setup our UI here. We implement the Handler callbacks which drive user interface updates. We wire up of the browser to Java plumbing. And, it starts up the Bluetooth stack.

Listing 11 onCreate method
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mHandler = new Handler() {
        public void handleMessage(Message inputMessage) {
            switch (inputMessage.what) {
                case NOTCONNECTED:
                    browser.loadUrl("javascript:setStatus('Not Connected');");
                    browser.loadUrl("javascript:setClassOnArduino('notconnected');");
                    browser.loadUrl("javascript:setActuating('notconnected');");
                    break;
                case SEARCHING:
                    browser.loadUrl("javascript:setStatus('Searching');");
                    browser.loadUrl("javascript:setClassOnArduino('searching');");
                    break;
                case FOUND:
                    browser.loadUrl("javascript:setStatus('Found');");
                    break;
                case CONNECTED:
                    browser.loadUrl("javascript:setStatus('Connected');");
                    browser.loadUrl("javascript:setClassOnArduino('discovering');");
                    break;
                case DISCOVERING:
                    browser.loadUrl("javascript:setStatus('Discovering');");
                    browser.loadUrl("javascript:setClassOnArduino('discovering');");
                    break;
                case COMMUNICATING:
                    browser.loadUrl("javascript:setStatus('Communicating');");
                    browser.loadUrl("javascript:setClassOnArduino('communicating');");
                    break;
                case TOGGLEDOOR:
                    browser.loadUrl("javascript:setActuating('communicating');");
                    break;
                case DISCONNECTING:
                    browser.loadUrl("javascript:setStatus('Disconnecting');");
                    break;
            }
        }
    };


    browser = (WebView) this.findViewById(R.id.browser);

    // set a webview client to override the default functionality
    browser.setWebViewClient(new wvClient());

    // get settings so we can config our WebView instance
    WebSettings settings = browser.getSettings();

    // JavaScript?  Of course!
    settings.setJavaScriptEnabled(true);

    // clear cache
    browser.clearCache(true);

    // this is necessary for "alert()" to work
    browser.setWebChromeClient(new WebChromeClient());

    // add our custom functionality to the javascript environment
    browser.addJavascriptInterface(new BLEUIHandler(), "bleui");

    // load a page to get things started
    browser.loadUrl("file:///android_asset/index.html");

    // Initializes Bluetooth adapter.
    final BluetoothManager bluetoothManager =
            (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    bluetoothAdapter = bluetoothManager.getAdapter();

    // Ensures Bluetooth is available on the device and it is enabled. If not,
    // displays a dialog requesting user permission to enable Bluetooth.
    if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
        Log.i(tag,"No BLE ??");
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, 1);
    }

}

The wvClient class acts as the glue between our Java and web components of this application. There is a single call from Java over to the Javascript side and this is also where we implement Java functions that are callable by the Javascript code housed in the index.html file.

Here is the required Javascript code to invoke the toggleDoor() function in the wvClient class:

 window.bleui.toggleDoor();

This WebViewClient implementation gets wired to the window object in the WebView under the name “bleui” with this call from the onCreate() method:

// add our custom functionality to the javascript environment
browser.addJavascriptInterface(new BLEUIHandler(), "bleui");

The “bleui” is entirely arbitrary – you can select anything you like there. Similarly, you can create as many namespaces as you like, each mapped to their own implementation of the WebViewClient interface.

In Listing 12 we are introduced to the code which responds to user actions.

  • onPageFinished() triggers the startup() javascript function found in index.html
  • interrogate() Java function that is triggered by go() javascript function
  • toggleDoor() Java function triggered by goopen() javascript function

Note that the functions update the UI via call to mHandler.sendEmptyMessage() and then trigger a BLE scan, passing in a distinct mode parameter to our own BLEFoundDevice class. The mode parameter drives subsequent behavior once the device is connected.

For more details on combining web development skills with Android development skills please see this tutorial, “Deliver hybrid Android applications with JSON.”

final class wvClient extends WebViewClient {
    public void onPageFinished(WebView view, String url) {
        // when our web page is loaded, let's call a function that is contained within the page
        // this is functionally equivalent to placing an onload attribute in the <body> tag
        // whenever the loadUrl method is used, we are essentially "injecting" code into the page when it is prefixed with "javascript:"
        browser.loadUrl("javascript:startup()");
    }
}

// Javascript handler
final class BLEUIHandler {
    @JavascriptInterface
    public void interrogate() {
        Log.i("BLEUI", "Initialize Scan");
        mHandler.sendEmptyMessage(SEARCHING);
        bluetoothAdapter.getBluetoothLeScanner().startScan(new BLEFoundDevice(INTERROGATE));
    }
    @JavascriptInterface
    public void toggleDoor() {
        Log.i("BLEUI", "Initialize Scan");
        mHandler.sendEmptyMessage(SEARCHING);
        bluetoothAdapter.getBluetoothLeScanner().startScan(new BLEFoundDevice(TOGGLEDOOR));
    }
}

In Listing 13 we handle the various events generated by the interaction of our Android device (BLE central) and the Arduino-based project (BLE peripheral) during the scanning process. This class implements the ScanCallback interface. Respective methods handle various outcomes of the scan. Look in particular at the onScanResult function. Depending on the mode (INTERROGATING versus TOGGLEDOOR), the function either queues up tasks to enumerate the services, characteristics and descriptors, or simply attempts to toggle the door by working directly with the service and characteristic of interest. Once the desired device has been found, we discontinue the scan so we do not get inundated with numerous scan results.

Working with BLE requires a lot of “callback” programming – this is one of the reasons we make use of the Handler interface for updating the UI.

When we initiate a scan for nearby devices, we provide an implementation of the ScanCallback interface. We call our class BLEFoundDevice.

Listing 13. BLEFoundDevice handles Bluetooth Scanning Results
final class BLEFoundDevice extends ScanCallback {
    private final String tag = "BLEDEVICE";
    private int mode = INTERROGATE;
    BLEFoundDevice(int mode) {
        this.mode = mode;
    }
    @Override
    public void onScanResult(int callbackType,ScanResult result) {
        Log.i(tag,"Found a device ==> " + result.toString());
        ScanRecord sr = result.getScanRecord();
        if (sr!= null) {
            if (sr.getDeviceName() != null) {
                if (sr.getDeviceName().equals("BLE Garage Opener")) {
                    bluetoothAdapter.getBluetoothLeScanner().stopScan(this);
                    mHandler.sendEmptyMessage(FOUND);
                    Log.i(tag, "Found our Garage Door Opener!");
                    BluetoothDevice remoteDevice = result.getDevice();
                    if (remoteDevice != null) {
                        String nameOfDevice = result.getDevice().getName();
                        if (nameOfDevice != null) {
                            Log.i(tag, "device is [" + nameOfDevice + "]");
                        }
                    }
                    Log.i(tag,"Advertise Flags [" + sr.getAdvertiseFlags() + "]");
                    List<ParcelUuid> solicitationInfo = sr.getServiceUuids();
                    for(int i=0;i<solicitationInfo.size();i++) {
                        ParcelUuid thisone = solicitationInfo.get(i);
                        Log.i(tag,"solicitationinfo [" + i + "] uuid [" + thisone.getUuid() + "]");
                    }
                    ParcelUuid [] services = remoteDevice.getUuids();
                    if (services != null)
                    {
                        Log.i(tag,"length of services is [" + services.length + "]");
                    }
                    // attempt to connect here
                    remoteDevice.connectGatt(getApplicationContext(),true,new BLERemoteDevice(mode));
                    Log.i(tag,"after connect GATT");
                } else {
                    Log.i(tag, "Not for us [" + sr.getDeviceName() + "]");
                }
            }
        } else {
            Log.i(tag,"Null ScanRecord??");
        }
    }
    @Override
    public void onScanFailed(int errorCode) {
        Log.e(tag, "Error Scanning [" + errorCode + "]");
    }
    @Override
    public void onBatchScanResults(List<ScanResult> results) {
        Log.i(tag,"onBatchScanResults " + results.size());
        for (int i=0;i<results.size();i++) {
            Log.i(tag,"Result [" + i + "]" + results.get(i).toString());
        }
    }
}

Continuing our use of callbacks, we interact with a connected peripheral with an implementation of the BluetoothGattCallback interface, named “BLERemoteDevice” in our application, shown in Listing 14. This class implements the BluetoothGattCallback interface to handle the results of the chatty back-and-forth communications between a BLE central and BLE peripheral. As results come back from the BLE peripheral, various methods in this class are invoked. Because GATT queries may not be overlapped, we implement a simple queue to hold the tasks requested. The tasks are enqueued in the BLEFoundDevice class and handled in the BLERemoteDevice class. A custom method called doNextThing is responsible for keeping things moving from one task to the next. Each element of the queue is an instance of the BLEQueueItem class.

Because GATT queries cannot overlap, which means we cannot issue more than one query at a time to a device, we build a queue, named “taskQ”, which is a LinkedList of the BLEQueueItem class, which we describe in Listing 15.

Listing 14. BLERemoteDevice handles GATT callbacks
final class BLERemoteDevice extends BluetoothGattCallback {
    private final String tag = "BLEDEVICE";
    UUID serviceWeWant = new UUID(0x0000FA0100001000L,0x800000805f9b34fbL);
    UUID toggleButtonUUID = new UUID(0x0000210200001000L ,0x800000805f9b34fbL);

    byte toggleDoorValue[] = {0x55};
    Queue<BLEQueueItem> taskQ = new LinkedList<BLEQueueItem>();
    private int mode = INTERROGATE;

    BLERemoteDevice(int mode) {
        this.mode = mode;
    }

    private void doNextThing(BluetoothGatt gatt) {
        Log.i(tag,"doNextThing");
        try {
            BLEQueueItem thisTask = taskQ.poll();
            if (thisTask != null) {
                Log.i(tag,"processing " + thisTask.toString());
                switch (thisTask.getAction()) {
                    case BLEQueueItem.READCHARACTERISTIC:
                        gatt.readCharacteristic((BluetoothGattCharacteristic) thisTask.getObject());
                        break;
                    case BLEQueueItem.WRITECHARACTERISTIC:
                        Log.i(tag,"Write out this Characteristic");
                        mHandler.sendEmptyMessage(TOGGLEDOOR);
                        BluetoothGattCharacteristic c = (BluetoothGattCharacteristic) thisTask.getObject();
                        Log.i(tag,"Value to be written is [" + c.getStringValue(0) + "]");
                       // c.setValue("U");
                        gatt.writeCharacteristic(c);
                        break;
                    case BLEQueueItem.READDESCRIPTOR:
                        gatt.readDescriptor((BluetoothGattDescriptor) thisTask.getObject());
                        break;
                    case BLEQueueItem.DISCONNECT:
                        mHandler.sendEmptyMessage(DISCONNECTING);
                        gatt.disconnect();
                        break;
                }
            } else {
                Log.i(tag,"no more tasks, peace out");
            }
        }
        catch (Exception e) {
            Log.i(tag,"Error in doNextThing " + e.getMessage());
        }
    }

    @Override
    public void onConnectionStateChange (BluetoothGatt gatt, int status, int newState) {
        Log.i(tag,"onConnectionStatChange [" + status + "][" + newState  + "]");
        if (status == BluetoothGatt.GATT_SUCCESS) {
            if (newState == BluetoothGatt.STATE_CONNECTED) {
                Log.i(tag,"Connected to [" + gatt.toString() + "]");
                mHandler.sendEmptyMessage(DISCOVERING);
                gatt.discoverServices();
            } else if (status == BluetoothGatt.STATE_DISCONNECTED) {
                mHandler.sendEmptyMessage((NOTCONNECTED));
            }
        }


    }
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        Log.i(tag,"OnServiceDiscovered ["+ status + "] " + gatt.toString());
        if (mode == INTERROGATE) {
            List<BluetoothGattService> services = gatt.getServices();
            for (int i = 0; i < services.size(); i++) {
                Log.i(tag, "service [" + i + "] is [" + services.get(i).getUuid().toString() + "]");
                if (serviceWeWant.equals(services.get(i).getUuid())) {
                    Log.i(tag, "************COOL, found it!!!");
                }
                UUID serviceUUID = services.get(i).getUuid();
                List<BluetoothGattCharacteristic> schars = services.get(i).getCharacteristics();
                for (int j = 0; j < schars.size(); j++) {
                    Log.i(tag, "characteristic [" + j + "] [" + schars.get(j).getUuid() + "] properties [" + schars.get(j).getProperties() + "]");
                    if ((schars.get(j).getProperties() & 2) == 2) {
                        taskQ.add(new BLEQueueItem(BLEQueueItem.READCHARACTERISTIC, schars.get(j).getUuid(), "Read Characteristic of Available Service", schars.get(j)));
                    } else {
                        Log.i(tag, "This Characteristic cannot be Read");
                    }
                    List<BluetoothGattDescriptor> scdesc = schars.get(j).getDescriptors();
                    for (int k = 0; k < scdesc.size(); k++) {
                        Log.i(tag, "Descriptor [" + k + "] [" + scdesc.get(k).toString() + "]");
                        Log.i(tag, "Descriptor UUID [" + scdesc.get(k).getUuid() + "]");
                        Log.i(tag, "Descriptor Permissions [" + scdesc.get(k).getPermissions() + "]");
                        //Log.i(tag,"Attempting to read this Descriptor");
                        taskQ.add(new BLEQueueItem(BLEQueueItem.READDESCRIPTOR, scdesc.get(k).getUuid(), "Read Descriptor of Characteristic", scdesc.get(k)));
                    }
                }
            }
        }

        if (mode == TOGGLEDOOR) {
            BluetoothGattService garageDoorOpener = gatt.getService(serviceWeWant);
            if (garageDoorOpener != null) {
                Log.i(tag, "Got it, woo hoo!!!");
                BluetoothGattCharacteristic toggleDoor = garageDoorOpener.getCharacteristic(toggleButtonUUID);
                if (toggleDoor != null) {
                    Log.i(tag, "Got the button");
                    Log.i(tag, "value is [" + toggleDoor.getStringValue(0) + "]");
                    toggleDoor.setValue(toggleDoorValue);
                    Log.i(tag, "value is [" + toggleDoor.getStringValue(0) + "]");
                    taskQ.add(new BLEQueueItem(BLEQueueItem.WRITECHARACTERISTIC, toggleDoor.getUuid(), "Write Characteristic to Toggle Door", toggleDoor));
                    //gatt.writeCharacteristic(toggleDoor);
                } else {
                    Log.i(tag, "No button");
                }
            } else {
                Log.i(tag, "No Service");
            }
        }
        Log.i(tag, "OK, let's go^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^");
        taskQ.add(new BLEQueueItem(BLEQueueItem.DISCONNECT, new UUID(0, 0), "Disconnect", null));
        mHandler.sendEmptyMessage(COMMUNICATING);
        doNextThing(gatt);
    }

    @Override
    public void onCharacteristicWrite (BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        Log.i(tag,"characteristic written [" + status + "]");
        if (characteristic.getUuid().equals(toggleButtonUUID)) {
            Log.i(tag,"value is [" + characteristic.getStringValue(0) + "]");
            if (characteristic.getStringValue(0).equals(("U"))) {
                Log.i(tag,"We're done here!");
            }
        }
        doNextThing(gatt);
    }

    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        Log.i(tag,"onCharacteristicChanged " + characteristic.getUuid());
    }

    @Override
    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        Log.i(tag,"characteristic read [" + characteristic.getUuid() + "] [" + characteristic.getStringValue(0) + "]");
        doNextThing(gatt);
    }

    @Override
    public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        try {
            Log.i(tag,"onDescriptorRead status is [" + status + "]");
            Log.i(tag, "descriptor read [" + descriptor.getCharacteristic().getUuid() + "]");
            Log.i(tag, "descriptor value is [" + new String(descriptor.getValue(), "UTF-8") + "]");
            doNextThing(gatt);
        } catch (Exception e) {
            Log.e(tag,"Error reading descriptor " + e.getStackTrace());
            doNextThing(gatt);
        }
    }
}

In Listing 15 we interact with our BLE peripheral. The BLEQueueItem is a convenient means for describing the relatively simple things we do with the peripheral in our application. A commercial, market-ready application would likely require additional attention, particularly to handling the scenario where a BLE peripheral does not respond to a GATT operation in a timely manner. We have no such timeout implementation in this tutorial.

Listing 15. BLEQueueItem holds tasks for iterative GATT interactions
package com.navitend.ble1;

import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;

import java.util.UUID;

public class BLEQueueItem {
    private final String tag = "BLEQueueItem";
    public static final int READCHARACTERISTIC = 0x01;
    public static final int WRITECHARACTERISTIC = 0x02;
    public static final int READDESCRIPTOR = 0x03;
    public static final int DISCONNECT = 0x04;



    BLEQueueItem(int action,UUID uuid,String comment,Object object) {
        Log.i(tag,"Adding new Queue Item " + comment + " [" + action + "] " + comment);
        this.action= action;
        this.uuid = uuid;
        this.comment= comment;
        this.object = object;
        Log.i(tag,"Adding new Queue Item " + this.toString());
        if (action == WRITECHARACTERISTIC) {
            BluetoothGattCharacteristic c = (BluetoothGattCharacteristic) object;
            Log.i(tag,"value to be written is [" + c.getStringValue(0) + "]");
        }

    }

    public String toString() {
        StringBuffer sb= new StringBuffer();
        sb.append("Action [" + action + "]");
        sb.append("UUID ["  + uuid.toString() + "]");
        sb.append("Comment [" + comment + "]");
        return sb.toString();
    }
    public int getAction() {
        return action;
    }
    public Object getObject() {
        return object;
    }

    private int action;
    private Object object;
    private UUID uuid;
    private String comment;
    private byte[] value;
}

OK, so that’s a fair amount of code. I would highly recommend downloading the projects from github and browsing around in the code.

Security, revisited

Earlier we briefly discussed the need for security, primarily for safety’s sake. However, we did not discuss any strategies for security. While we don’t have the space to go into depth on this, it seems only fair to discuss an idea or two on how to best implement security in a BLE solution like the one I describe in this tutorial.

Security is best when it is done in layers. We call this “Defense in Depth.”

Device discovery

In our application, we are searching for a device with a specific device name. Then, we look for a specific service. Once found, we simply fire a value at it. This approach might work fine at a public kiosk that is meant for entertainment or to issue some basic information. However, this approach would not be appropriate for a “move this 300 pound door up and down” type of application. In this scenario, you would likely want to have some sort of registration step to permit only eligible devices to interact with your device, not unlike the pairing process with a commercial garage door. In our example, the actual door security is taken care of by the fact that the remote control we use was previously paired with my garage door opener. Running this application with a newly purchased, unconfigured garage door opener will not actually open a door!

We may also consider making the discovery process more complicated than working by name, but go deeper into the service and characteristic hierarchy. Perhaps you could look for a specific name that was changed when the device was deployed. For example, how many wifi networks have you seen that are called, “iPhone”. They are everywhere! Because we can interrogate a device by looking for specific characteristics (and their values), it can get arbitrarily complex.

Characteristic writing

We actuated the remote control via a simple button press, but you might consider adding a few safeguards prior to writing a characteristic, in addition to the device discovery step above. Here are some ideas:

  • A confirmation code or popup on the central device would limit the accidental interaction, but using a commercial utility would easily thwart this step.
  • A password set by the user on one or both sides of the application. Security is best with BLE if the peripheral device participates.
  • A thoughtful allocation of characteristics. Require a specific sequence of values to be written before taking a step. For example, write a value to characteristic A before writing a value to characteristic B in order to take an action.
  • Require a device to be connected for a minimum amount of time before permitting a specific action. This simple step can limit the drive-by actuation.
  • Implement a lock-out feature if any of the above steps fail, though be sure to have your lockout have a time-out such that you don’t turn your product into a brick.

Well executed security is a matter of matching the risk profile to the steps required to perform the operation. If the consequences are high, then make the steps to take the operation high. Note that this might mean a high fence for device association or pairing, but a low fence for the actual operation.

Summary

This tutorial presented an example of using the Android BLE capabilities to interact with an Arduino-based BLE peripheral device. The functionality demonstrates interacting with a commercial garage door, but the bigger ambition was to demonstrate adding a user interface to a modern device without the cost or trouble of embedded screens and buttons.