Interact with Bluetooth Devices using the Web Bluetooth API

August 1, 2020 | 7 minutes
Bluetooth speaker
Photo by Burst from Pexels
TL;DR: Try the Web Bluetooth API in the browser or check out the sample React app: https://github.com/rdeprey/web-bluetooth-starter.

If you have smart home devices, you’ve likely interacted with them over Bluetooth from a native app on your phone. Did you know that you can also interact with Bluetooth devices through a website? You can using the Web Bluetooth API.

Getting a device’s battery level with the Web Bluetooth API
Getting a device’s battery level with the Web Bluetooth API

Although this API is still considered experimental technology, it has decent support in Google Chrome, Edge, and Opera. Some of the things you can do with the Web Bluetooth API include:

  • Requesting and connecting to nearby devices
  • Reading and writing Bluetooth characteristics
  • Receiving GATT notifications
  • Receiving notifications when a device is disconnected
  • Reading and writing Bluetooth descriptors

If you’re new to how Bluetooth devices work, I’ll explain some of the concepts above. Otherwise, feel free to skip ahead.

Bluetooth Key Concepts

Here are a few key concepts you’re likely to come across as you start working with the Web Bluetooth API.

GATT (Generic Attribute Profile)

GATT plays an integral role in defining how data is shared over a Bluetooth connection. It describes the data transfer processes and formats. Even more important, it provides a reference framework that’s used for all devices with GATT-based profiles to ensure that devices from different vendors work together.

GATT Client vs. Server

The GATT client sends requests to a Bluetooth device and gets responses from it. In our case, the GATT client is a web browser that supports the Web Bluetooth API.

The GATT server gets requests from a client and returns data. It can also be configured to send data on its own without receiving a request from a client. Bluetooth devices generally serve this role.

Sharing Data

When sharing data between Bluetooth devices, GATT organizes the data into characteristics, services, and descriptors.

Characteristics Characteristics hold user data and have at least two attributes:

  • Characteristic name Describes what the characteristic is, such as battery level or heart rate
  • The value for that characteristic

Services GATT services are collections of related characteristics. For example, you could have a service called “Battery” that includes characteristics like the current battery level and the estimated amount of time remaining before the battery needs recharging.

Descriptors Descriptors are defined attributes that describe a characteristic value. They can be used to provide information that makes it easier to understand what a characteristic’s value represents. For example, a descriptor could explain the expected range for a value or a unit of measure being used for a value.

GATT Notifications

GATT notifications let a website or application know when a particular characteristic changes on a device. For example, a web page that displays the battery level for a Bluetooth device could subscribe to notifications that indicate changes in the paired device’s battery level.

Security Protections

In order to use the Web Bluetooth API, there are a few requirements to protect the privacy of users:

  • The web page must be hosted with HTTPS You can interact with the API on http://localhost for testing, but HTTPS will need to be configured for the page when it’s hosted.
  • The user has to make a gesture of some sort (click, touch, etc.) in order for the web page to discover Bluetooth devices

Connecting to a Bluetooth Device from the Browser

Now that you have some background information on how Bluetooth devices communicate with each other, let’s build something!

For this example, let’s write some JavaScript that gets a device’s battery level using the Web Bluetooth API and displays it on a web page.

A Note on Testing Only devices that actively advertise a battery service will appear in the list of devices the browser can pair with for this example. I found that my Galaxy S9+ on it’s own doesn’t seem to advertise the battery service, so I used a simulator to test. An easy way to try this is to use a BLE simulator like BLE Peripheral Simulator (Android) or My BLE Simulator (Apple).

The Code To get started, create an asynchronous function that will search for Bluetooth devices and connect to the one selected.

const connectToDeviceAndSubscribeToUpdates = async () => {}

Inside the function, let’s start by requesting devices that advertise a battery service. The navigator.bluetooth.requestDevice function takes an options object where you can specify what type of devices to scan for. In our case, we want to scan for devices that advertise their battery service, so that we can get the device’s battery level.

const connectToDeviceAndSubscribeToUpdates = async () => {
    const device = await navigator.bluetooth
    .requestDevice({
        filters: [{ services: ['battery_service']}
    });
};

There are lots of other ways to filter devices, including by device name, service UUID (handy when you make your own GATT Server on a Bluetooth peripheral like a Raspberry Pi), or 16-bit service ID. You can also use multiple filters of the same or different types to filter the devices available for pairing. Take a look at the list of the standard GATT Services and try using different filters to get data from your devices.

Next, add an HTML button element to your web page and set its onClick function to call the connectToDeviceAndSubscribeToUpdates function. To pair with a Bluetooth device from the browser, you have to have a way for the user to initiate the action such as by clicking on a button.

<button onClick="connectToDeviceAndSubscribeToUpdates()">Connect to Bluetooth device</button>

Clicking on the button and calling the navigator.bluetooth.requestDevice function triggers the browser to show a popup that lists the devices that meet the specified filters.

Popup showing devices that meet our filter requirements
Popup showing devices that meet our filter requirements

At this point, you can select a device from the list and pair it with the browser. Nothing will happen yet though. We need to try to connect to the GATT server that’s running on the Bluetooth device.

const connectToDeviceAndSubscribeToUpdates = async () => {
    const device = await navigator.bluetooth
    .requestDevice({
        filters: [{ services: ['battery_service']}
    });
    const server = await device.gatt.connect();
};

After we’re connected to the GATT server, we need to get the battery service from the server.

const connectToDeviceAndSubscribeToUpdates = async () => {
    const device = await navigator.bluetooth
    .requestDevice({
        filters: [{ services: ['battery_service']}
    });
    const server = await device.gatt.connect();
    const service = await server.getPrimaryService('battery_service');
};

With the service now available, we can get the battery level characteristic from the device.

const connectToDeviceAndSubscribeToUpdates = async () => {
    const device = await navigator.bluetooth
    .requestDevice({
        filters: [{ services: ['battery_service']}
    });
    const server = await device.gatt.connect();
    const service = await server.getPrimaryService('battery_service');
    const characteristic = await service.getCharacteristic('battery_level');
};

At this point, we have what we need to be able to get the battery level for the device. To do that, we use the readValue function on the characteristic. The value is a DataView of an ArrayBuffer, so we use the getUint8 function to get an individual byte from the array at the index passed in, which is 0 in our case.

const connectToDeviceAndSubscribeToUpdates = async () => {
    const device = await navigator.bluetooth
    .requestDevice({
        filters: [{ services: ['battery_service']}
    });
    const server = await device.gatt.connect();
    const service = await server.getPrimaryService('battery_service');
    const characteristic = await service.getCharacteristic('battery_level');

    const reading = await characteristic.readValue();
    console.log(reading.getUint8(0) + '%');
};

The function above will now get the battery level reading from the paired device and log it to the browser’s console.

This is great, but so far our code will only get a reading one time, right after the device connects. We can subscribe to updates (aka GATT notifications) by calling the startNotifications function on the characteristic. Adding an event handler for the characteristicvaluechanged event will allow us to update the web page with the new value as the battery level changes.

const handleCharacteristicValueChanged = (event) => {
    console.log(event.target.value.getUint8(0) + '%');
};

const connectToDeviceAndSubscribeToUpdates = async () => {
    const device = await navigator.bluetooth
    .requestDevice({
        filters: [{ services: ['battery_service']}
    });
    const server = await device.gatt.connect();
    const service = await server.getPrimaryService('battery_service');
    const characteristic = await service.getCharacteristic('battery_level');

    characteristic.startNotifications();
    characteristic.addEventListener('characteristicvaluechanged', handleCharacteristicValueChanged);

    const reading = await characteristic.readValue();
    console.log(reading.getUint8(0) + '%');
};

Try this out with the example site or download a React starter app: https://github.com/rdeprey/web-bluetooth-starter.

Those are the basics of connecting to Bluetooth devices from the browser. This is just the start though. You can write your own GATT services to share data over Bluetooth between microcontrollers and other IoT devices.

Additional Resources