Using Web Bluetooth To Read BBQ Temperature Sensor Data

In this short read we’ll explore the Web Bluetooth API. We’ll connect with my Bluetooth BBQ thermometer. Then we’ll try to read out its data and listen to its temperature updates. At the end of the article you’ll have a good idea about what you can do with the Web Bluetooth API.

A couple months ago we purchased the Hermanos Grill 5.0 Bluetooth BBQ thermometer. It supports up to 6 temperature sensors, in this article I’ll connect 2. It also supports Bluetooth so you can connect your phone and see BBQ temperatures without having to get out of your chair.

Ahhhh, we’re truly living in the future aren’t we!

Connecting To The Device

At this time the Web Bluetooth API is only supported by Chrome. Unfortunately there’s no signal from other vendors that they’re working on adding support.

So much for living in the future. 🤷‍♂️

The first thing we need to do is connect to the Hermanos device over Bluetooth, we can do that by calling navigator.bluetooth.requestDevice()

We can pass filter parameters to have the browser only show the devices we’re interested it (we should do this in production), but for now we set acceptAllDevices to true so we can see every device around us.

const device = await navigator.bluetooth.requestDevice({
    // Show us all the things!
    acceptAllDevices: true,
});

The Web Bluetooth API requires a user interaction as trigger to connect, for security reasons you can’t just connect to a device on page load.

The user needs to click something that initiates the connection request.

Let’s add a connect button.

<button type="button" onclick="connect()">Connect</button>

<script>
    async function connect() {
        const device = await navigator.bluetooth.requestDevice({
            acceptAllDevices: true,
        });
    }
</script>

Pretty straightforward.

Click our “Connect” button and Chrome opens a panel to select a Bluetooth device.

A screenshot of the Chrome Bluetooth Pair panel showing the devices you can pair with

With the device connection set up, let’s move on to the next steps:

I had to look up the GATT acronym because I just can’t pass on an opportunity to use the <abbr> tag.

GATT is an acronym for “Generic ATTribute Profile”.

It sets the terms for communication between two Bluetooth devices. So now you know.

Connecting To The Bluetooth GATT Server

There’s nothing more to it than calling device.gatt.connect()

async function connect() {
    const device = await navigator.bluetooth.requestDevice({
        acceptAllDevices: true,
    });

    // Connect to the GATT server
    const server = await device.gatt.connect();
}

Now that we have a reference to the GATT server, we can request the primary service.

At this point, I struggled a tiny bit.

To get the primary service we call server.getPrimaryService()

async function connect() {
    const device = await navigator.bluetooth.requestDevice({
        acceptAllDevices: true,
    });

    const server = await device.gatt.connect();

    // We fetch the primary service
    const service = await server.getPrimaryService();
}

This function however expects a Universally Unique Identifier.

It should be called like this getPrimaryService(uuid)

I had no idea where to get this UUID (another <abbr> 🥳), as I don’t have any documentation on this device I’m trying to connect to.

After some looking around I stumbled uppon a handy Chrome utility. You can navigate to chrome://bluetooth-internals, connect to a device, and inspect it.

Chrome queries the device and displays an overview of the available internals:

A screenshot of the Chrome Bluetooth Internals showing the device, services, characteristics and their matching identifiers

I just love how hardcore that interface is. Zero design. Those blue bar are clickable, it’s an accordion. The inspect button doesn’t always work, or inspects the incorrect device. Let’s say it’s probably a work in progress.

But while it doesn’t look great, it’s functional. We can see the service with Type set to Primary, and it displays that UUID we’ve been looking for.

We need to pass the UUID as a parameter to getPrimaryService, and we need to inform the device we’re going to do that by adding it to the optionalServices array when we call requestDevice

// 1. Store the UUID in a constant so we can use it more easily
const PRIMARY_SERVICE_UUID = '0000ffb0-0000-1000-8000-00805f9b34fb';

async function connect() {
    const device = await navigator.bluetooth.requestDevice({
        acceptAllDevices: true,

        // 2. Tell the device which services we want to use
        optionalServices: [PRIMARY_SERVICE_UUID],
    });

    const server = await device.gatt.connect();

    // 3. Fetch the primary service by UUID
    const service = await server.getPrimaryService(PRIMARY_SERVICE_UUID);
}

Just like that the service variable contains a reference to the primary BluetoothRemoteGATTService

This GATT service has characteristics, we can see this in the Chrome Bluetooth Internals accordion.

Let’s subscribe to the temperature characteristic for notifications!

Subscribing To Bluetooth Characteristic Notifications

This specific BBQ temperature thingy has a notification characteristic, meaning it sends out temperature information (and probably some other stuff), and we can subscribe to those notifications.

We know this because we can see it in the Chrome Bluetooth Internals accordion. It shows this nice green checkmark next to the Notify property.

A screenshot of the characteristic in the Chrome Bluetooth Internals panel showing a list of properties with checkmarks and crosses to indicate which features are supported

Let’s get access to the notification characteristic by calling getCharacteristic() and passing the characteristic UUID.

const PRIMARY_SERVICE_UUID = '0000ffb0-0000-1000-8000-00805f9b34fb';

// 1. Add another constant
const NOTIFICATION_CHARACTERISTIC_UUID = '0000ffb2-0000-1000-8000-00805f9b34fb';

async function connect() {
    const device = await navigator.bluetooth.requestDevice({
        acceptAllDevices: true,
        optionalServices: [PRIMARY_SERVICE_UUID],
    });

    const server = await device.gatt.connect();

    const service = await server.getPrimaryService(PRIMARY_SERVICE_UUID);

    // 2. Get the notification characteristic from the primary service
    const characteristic = await primaryService.getCharacteristic(
        NOTIFICATION_CHARACTERISTIC_UUID
    );
}

Now that we have our Characteristic we can listen for its 'characteristicvaluechanged' event.

const PRIMARY_SERVICE_UUID = '0000ffb0-0000-1000-8000-00805f9b34fb';

const NOTIFICATION_CHARACTERISTIC_UUID = '0000ffb2-0000-1000-8000-00805f9b34fb';

async function connect() {
    const device = await navigator.bluetooth.requestDevice({
        acceptAllDevices: true,
        optionalServices: [PRIMARY_SERVICE_UUID],
    });

    const server = await device.gatt.connect();

    const service = await server.getPrimaryService(PRIMARY_SERVICE_UUID);

    const characteristic = await primaryService.getCharacteristic(
        NOTIFICATION_CHARACTERISTIC_UUID
    );

    // Listen to the change event
    characteristic.addEventListener('characteristicvaluechanged', (e) => {
        // I'm called when the value changes
    });
}

To get this show on the road we need to tell our characteristic to start notifying, this we can do with the startNotifications method.

const PRIMARY_SERVICE_UUID = '0000ffb0-0000-1000-8000-00805f9b34fb';

const NOTIFICATION_CHARACTERISTIC_UUID = '0000ffb2-0000-1000-8000-00805f9b34fb';

async function connect() {
    const device = await navigator.bluetooth.requestDevice({
        acceptAllDevices: true,
        optionalServices: [PRIMARY_SERVICE_UUID],
    });

    const server = await device.gatt.connect();

    const service = await server.getPrimaryService(PRIMARY_SERVICE_UUID);

    const characteristic = await primaryService.getCharacteristic(
        NOTIFICATION_CHARACTERISTIC_UUID
    );

    // Tell it to start sending update notifications
    await characteristic.startNotifications();

    // Then we listen for changes
    characteristic.addEventListener('characteristicvaluechanged', (e) => {
        // I'm called when my value changes
    });
}

The cached value of the notification characteristic is stored as a DataView in the characteristic.value property.

For clarity we’ll detach the event handler function and log the value.

const PRIMARY_SERVICE_UUID = '0000ffb0-0000-1000-8000-00805f9b34fb';

const NOTIFICATION_CHARACTERISTIC_UUID = '0000ffb2-0000-1000-8000-00805f9b34fb';

async function connect() {
    const device = await navigator.bluetooth.requestDevice({
        acceptAllDevices: true,
        optionalServices: [PRIMARY_SERVICE_UUID],
    });

    const server = await device.gatt.connect();

    const service = await server.getPrimaryService(PRIMARY_SERVICE_UUID);

    const characteristic = await primaryService.getCharacteristic(
        NOTIFICATION_CHARACTERISTIC_UUID
    );

    await characteristic.startNotifications();

    // Handles the 'characteristicvaluechanged' event
    const handleValueChanged = (e) => {
        // Updated value
        console.log(characteristic.value);
    };

    // Move the event handler to a separate function
    characteristic.addEventListener(
        'characteristicvaluechanged',
        handleValueChanged
    );
}

In our developer console we can see the following log.

DataView(15)

We have some data to work with! Let’s try to read the sensor temperatures from it.

Getting Data From The BlueTooth Characteristic DataView

A DataView. What is this, and how do we get the temperature information from it? 🤔

The DataView contains the binary data returned by the notification characteristic. If you have a manual telling you which data is where, that’s useful, but I didn’t have one.

With some experimentation and perseverance we can figure this out.

In the developer console we can expand DataView, then buffer, then [[Int8Array]]

A screenshot of the Chrome Memory inspector, it shows memory addresses and their values converted to various formats like int, uint, float, and string

To figure out which data correlated to which sensor I detached all sensors and then plugged in each one while watching for changes.

  1. When I attached sensor 1, the values of index 2 changed from -1 to 1, and 3 changed from -1 to an integer.
  2. The same happened for sensor 2, but it updated indexes 4 and 5

So looks like that’s where the device stores the sensor data. Our next step is to convert these values to temperatures.

This took me over an hour to figure out. For some reason I decided to click the little computer chip next to DataView and Chrome opened the Memory inspector.

Honestly. This caught me off guard. I’ve logged DataView instances before but can’t remember seeing this icon, perhaps it’s new?

Anyway, here’s the panel in all its glory:

A screenshot of the Chrome Memory inspector, it shows memory addresses and their values converted to various formats like int, uint, float, and string

This is super useful.

We can click through the memory locations and see what value each location contains. The 3F location in the top right converts to 319 and that correlates quite nicely with the 32 ℃ the device display shows for sensor 1.

319 / 10 = 31,9

Plausible. Let’s read those bytes.

const handleValueChanged = (e) => {
    // We get the value at index 2 as `Uint16`
    const sensorOneTemp = characteristic.value.getUint16(2) / 10;

    // We get the value at index 4 as `Uint16`
    const sensorTwoTemp = characteristic.value.getUint16(4) / 10;

    // Log the information to the dev console
    console.clear();
    console.log('characteristicvaluechanged');
    console.log('#1', sensorOneTemp);
    console.log('#2', sensorTwoTemp);
};

And presto, we can see the result below. The temperature for sensor 2 increases when I hold it between my fingers.

You can inspect the final code snippet below.

<button type="button" onclick="connect()">Connect</button>

<script>
    const PRIMARY_SERVICE_UUID = '0000ffb0-0000-1000-8000-00805f9b34fb';

    const NOTIFICATION_CHARACTERISTIC_UUID =
        '0000ffb2-0000-1000-8000-00805f9b34fb';

    async function connect() {
        const device = await navigator.bluetooth.requestDevice({
            acceptAllDevices: true,
            optionalServices: [PRIMARY_SERVICE_UUID],
        });

        const server = await device.gatt.connect();

        const service = await server.getPrimaryService(PRIMARY_SERVICE_UUID);

        const characteristic = await primaryService.getCharacteristic(
            NOTIFICATION_CHARACTERISTIC_UUID
        );

        await characteristic.startNotifications();

        const handleValueChanged = (e) => {
            const sensorOneTemp = characteristic.value.getUint16(2) / 10;
            const sensorTwoTemp = characteristic.value.getUint16(4) / 10;

            console.clear();
            console.log('characteristicvaluechanged');
            console.log('#1', sensorOneTemp);
            console.log('#2', sensorTwoTemp);
        };

        characteristic.addEventListener(
            'characteristicvaluechanged',
            handleValueChanged
        );
    }
</script>

This is a real Connect button, it’s linked up to the finished script.

If you own the same sensor, in theory, you should be able to connect to it on this page.

Conclusion

It’s crazy to me how powerful web browsers have become, we linked up a Bluetooth device in under 10 lines of code. 🤯

If you followed along you should have a good idea now about how you could use the Bluetooth Web API, there’s plenty of other and probably more useful use cases out there. Let me know what you decided to connect to!

Let’s keeps our fingers crossed that Apple and Mozzila add support for the Web Bluetooth API soon so we can start creating truly cross platform web apps.

I share web dev tips on Twitter, if you found this interesting and want to learn more, follow me there

Or join my newsletter

More articles More articles