Share

engineering

8min read

Web Bluetooth—Interacting with Texas Instruments SensorTag

Web Bluetooth—Interacting with Texas Instruments SensorTag

Introduction

We’re surrounded by BLE (Bluetooth Low Energy) devices, such as smartwatches, smart light bulbs, smart sensors, smart… the list goes on. Would you believe that you can interact with all these appliances via a web browser? Now, it’s possible thanks to Web Bluetooth—a simple JavaScript API. Interested?

In this article, I’ll briefly go over technicalities of BLE technology and present you a basic usage of Web Bluetooth API. We’ll connect to a BLE device, explore its services, and read some measurements from a sensor… all with a browser! Are you ready?

GAP

First, let’s go over some basic concepts from the world of Bluetooth Low Energy. We can distinguish two roles of devices: peripheral and central. Peripheral devices, i.e. sensor tags, smart light bulbs or heart rate monitors, usually run on low power and have limited resources.

Peripherals advertise their presence to devices which perform the central role—typically bigger, more powerful units, such as a smartphone or a personal computer. Later in this article, I’ll walk you through connecting a simple BLE sensor tag to a web browser. Stay put!

One thing that’s crucial to understand here is that a connection can only be made between a central and a peripheral device. You cannot connect two central or two peripheral devices. There’s one more restriction. If your peripheral device doesn’t support Bluetooth 4.2 or newer, it can be connected only to one central at a time.

How does a peripheral advertise its data? What happens under the hood when a central device wants to make a connection? For this and more technical details I suggest you check out the GAP (Generic Access Profile) description made by Texas Instruments.

Using JavaScript API

GATT

There’s one more thing that we should take a look at before we start coding, namely GATT. GATT, or Generic Attribute Profile, is a protocol stack which defines how the BLE devices communicate. It introduces two concepts that we’ll be working with closely: services and characteristics.

Services are nothing more than groups of one or more characteristics. For instance, an accelerometer service can contain a characteristic with sensor output data, a config characteristic telling us whether the notifications are enabled, and another config characteristic that includes information about the period of measurements. Each service can be identified with a 16, 32 or 128-bit UUID (a unique numeric ID). 16-bit UUIDs are specific for standardized services, a full list of which you can find here.

Characteristics contain bytes of data that should be interpreted according to the device’s documentation. For example, data stored in one of the characteristics of my SensorTag’s accelerometer represents values of acceleration on X, Y, and Z axes. Just like the services, the standardized characteristics are identified by a unique 16-bit UUID, whereas non-standardized UUIDs have 32 or 128 bits.

What’s great about characteristics is that we’re able to not only read a single value but also subscribe to notifications and write. How and why would we want to do that? Read on to find out!

For a more detailed explanation of how services and characteristics work, check out this riveting article on BLE sniffing written by Dariusz Seweryn, our Staff Software Engineer.

Let’s code!

Alright, now that we have the slightly less interesting part behind us, let’s use it in practice. What you’ll need is a compatible web browser and a BLE peripheral. In my case, it’s the Texas Instruments SensorTag CC2541. How do we find the desired UUIDs? It’s simple; a quick look into the documentation and an attribute table of your device will provide all the necessary information. Not only the list of identifiers but also descriptions and read/write/notify permissions for services and characteristics.

But wait, there’s an even simpler way! If you’re a Google Chrome user, go to chrome://bluetooth-internals/#devices, click Start scan and… there you have a list of every Bluetooth device in your range. Find the desired one, and there you have it–all available services and characteristics with descriptors. Awesome, isn’t it?

The whole Web Bluetooth API is based on JavaScript Promises. If you haven’t encountered Promises before, I strongly encourage you to get familiar with the concept before reading further. In this article, I’m going to use the async await syntax, which—in my opinion—is more readable than chained then statements.

A Mac laptop screen, on the desktop — JavaScript API

Scanning devices

How do we trigger a device scan? Simply by calling the navigator.bluetooth.requestDevice method. However, there’s a catch. Due to security reasons, the scan has to be triggered by a user gesture, such as a button click.

The requestDevice(options) function takes an object as an argument. Building the options object might be tricky, so let’s start from the beginning, namely the documentation.

dictionary RequestDeviceOptions {
  sequence<BluetoothLEScanFilterInit> filters;
  sequence<BluetoothServiceUUID> optionalServices = [];
  boolean acceptAllDevices = false;
};
  • The function will return only the devices that match the criteria passed in filters array. Typically, a filter object specifies the device’s name or a list of required services.
  • optionalServices specifies the list of UUIDs of services that we might want to access,
  • If acceptAllDevices is true, every Bluetooth device in range will be available for connection.
  • requestDevice() function returns a Promise that resolves to a device object
  • If a service is on the list of standardized services, you can provide its name rather than UUID–optionalServices: [“battery_service”]

A bit confusing, right? To clear things up, I’ll demonstrate how to access the accelerometer on my SensorTag. Naturally, the process is similar for any device and any service.

try {
 const device = await navigator.bluetooth.requestDevice({
   filters: [{ name: "SensorTag" }],
   optionalServices: [SERVICE_UUID]
 });
} catch (err) {
 console.log(err.message);
}

Let’s go through the snippet above step-by-step.

I look for a device called “SensorTag” and indicate that later I will want to access the specified service. Here comes the first gotcha—we’re able to access ONLY the services that were passed as either optionalServices or as filters. If I didn’t include the optionalServices in the options object, I could successfully connect to my SensorTag. I wouldn’t be, however, allowed to access any service.

The function call opens a modal dialog in the browser and performs a scan. Devices that match the filter criteria are displayed and available for connection.

Here are some alternative ways of constructing the options object.

 {
   filters: [{ services: [SERVICE_UUID]}]
};

Specifying services array as a filter will look only for devices that advertise those services. It looks more concise than the example above, doesn’t it? So why didn’t I use it? My sensor tag doesn’t advertise its services before making a connection, thus the device scan couldn’t find it. That’s something to look out for!

{
   optionalServices: [SERVICE_UUID],
   acceptAllDevices: true
 };

Setting acceptAllDevices will, no surprise here, allow you to connect to any device in range. It’s your job to choose the correct one. Here comes the second gotcha—you have to either pass a filters array or set acceptAllDevices. Doing both will result in an error. If you’re not sure which option is best for you, go with the first one. Using acceptAllDevices will result in displaying lots of unrelated devices and wasted energy.

Getting service and characteristics

Now that we have found our peripheral device, we can connect to it. Once we do so, it becomes a GATT server. From the GATT server, we can acquire the service. Remember, the UUID has to be among the ones you provided when scanning for device!

const device = /* code for getting the device */
const server = await device.gatt.connect();
const service = await server.getPrimaryService(SERVICE_UUID);

Another method of acquiring services is calling server.getPrimaryServices method, which returns an array of available services.

It’s time for the exciting part, namely reading and writing to characteristics. I need two of them—accelerometer config characteristic to start measurements and accelerometer data characteristic to read the sensor’s output. If you’re not sure which characteristics you need, as always, take a peek at your device’s documentation.

const configCharacteristic = await service.getCharacteristic(
 CONFIG_CHARACTERISTIC_UUID
);

const dataCharacteristic = await service.getCharacteristic(
 DATA_CHARACTERISTIC_UUID
);

Let’s enable the measurements by writing “1” to config characteristic…

await configCharacteristic.writeValue(Uint8Array.of(1)); // enable acc sensor

…and read the output.

const accValue = await dataCharacteristic.readValue();

That’s right—it’s as simple as that! Reading and writing from/to characteristics is just a matter of calling readValue and writeValue functions.

Subscribing to notifications is just as easy. All we need to do is call startNotifications function on the data characteristic object and listen to characteristicvaluechanged event.

await dataCharacteristic.startNotifications();
dataCharacteristic.addEventListener(
 "characteristicvaluechanged",
 dataHandlerFunction
);

Disconnect from GATT server

GATT server disconnects automatically, but we’re able to trigger it manually with device.gatt.disconnect method. Upon disconnecting, a gattserverdisconnected event is triggered, which might be useful for displaying a warning message or prompting the user to reconnect.

There’s one thing to watch out for here. Once the server disconnects, all services and characteristics get invalidated. That means that we can’t reuse them upon reconnecting to the device. gattserverdisconnected event listener would be a good place to run the setup.

device.addEventListener('gattserverdisconnected', getServicesAndCharacteristics);

Browsers compatibility

So far everything about the Web Bluetooth API was sunshine and rainbows. Unfortunately, there’s one big caveat—browser compatibility. Right now, the API is available only on Chrome, Opera (and their Android counterparts), Samsung Internet and Android Browser. Firefox, Edge, Internet Explorer and the browsers based on WebKit (i.e. Safari or Chrome for iOS) don’t support the technology. That’s a huge bummer and we can only hope that in the future Web Bluetooth will be available on all modern browsers.

Here you can check the current implementation status on various web browsers.

Conclusion

Has Web Bluetooth caught your interest? Even though the API is still in development, it shows great potential and we can expect its popularity to explode once it’s implemented on every mainstream browser. Until then, the technology is limited to not-so-serious, but very entertaining projects. I mean, just look at this cool video of toy racing cars being controlled through a web app.

If you’d like to see more use cases of Web Bluetooth, check out the samples prepared by Google developers. And if you have any questions about Bluetooth Low Energy, don’t hesitate to contact us directly.

Share

Kamil

Junior Software Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!