Getting started with Web Bluetooth in React

Dec, 02 2022

Disclaimer: Web Bluetooth is an experimental technology. I'm not quite sure it's ready for production yet, but has been a lot of fun to play with.

For my peripheral device, I am using a Seeed XIAO nRF52840.

The Xiao nRF52840 has Circuit Python loaded onto the device. You can follow our guide for details on how to do this.

Load the two required libraries for this sample code: adafruit_ble and adafruit_ble_adafruit.

You can download the Circuit Python library bundle for Version 7.x here.

The peripheral device has a simple program running on it to simulate sensor data:

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT # Use with Web Bluetooth Dashboard, or with ble_adafruit_simpletest_client.py import time import microcontroller from adafruit_ble import BLERadio from adafruit_ble_adafruit.adafruit_service import AdafruitServerAdvertisement from adafruit_ble_adafruit.temperature_service import TemperatureService temp_svc = TemperatureService() temp_svc.measurement_period = 100 temp_last_update = 0 ble = BLERadio() # Unknown USB PID, since we don't know what board we're on adv = AdafruitServerAdvertisement() adv.pid = 0x0000 while True: # Advertise when not connected. print(adv) print(bytes(adv)) ble.start_advertising(adv) while not ble.connected: pass ble.stop_advertising() while ble.connected: now_msecs = time.monotonic_ns() // 1000000 # pylint: disable=no-member if now_msecs - temp_last_update >= temp_svc.measurement_period: temp_svc.temperature = ( microcontroller.cpu.temperature # pylint: disable=no-member ) temp_last_update = now_msecs

Setting up web bluetooth with React

I'll be bootstrapping from a NextJS repository that already has Tailwind and DaisyUI configured.

I recommend starting with this repository as well. Otherwise, feel free to work in a React environment of your choice.

Once you've cloned the starter repo into your workspace (and performed an npm install && npm run dev), navigate to index.js and replace the contents with the following:

import { useState, useEffect } from 'react' export const ADAFRUIT_TEMPERATURE_SERVICE_UUID = 'ADAF0100-C332-42A8-93BD-25E905756CB8'.toLowerCase() export const ADAFRUIT_TEMPERATURE_CHARACTERISTIC_UUID = 'ADAF0101-C332-42A8-93BD-25E905756CB8'.toLowerCase() export default function Home () { const [supportsBle, setSupportsBle] = useState(false) const [connectedDevice, setConnectedDevice] = useState(null) const [isLoading, setIsLoading] = useState(false) const [temperature, setTemperature] = useState(0) useEffect(() => { if (navigator.bluetooth) { setSupportsBle(true) } }, []) const onDisconnected = (event) => { setConnectedDevice(null) } const notifyEventChange = async (e) => { const val = e.currentTarget.value.getFloat32(0, true) setTemperature(val) } const connectToDevice = async () => { if (!supportsBle) { return } try { const device = await navigator.bluetooth.requestDevice({ filters: [{ namePrefix: 'CIRCUITPY' }], optionalServices: [ADAFRUIT_TEMPERATURE_SERVICE_UUID] }) if (!device) { throw new Error() } device.addEventListener('gattserverdisconnected', onDisconnected) setIsLoading(true) const server = await device.gatt.connect() const temperatureService = await server.getPrimaryService(ADAFRUIT_TEMPERATURE_SERVICE_UUID) const tempCharacteristic = await temperatureService.getCharacteristic(ADAFRUIT_TEMPERATURE_CHARACTERISTIC_UUID) await tempCharacteristic.startNotifications() tempCharacteristic.addEventListener('characteristicvaluechanged', notifyEventChange) setConnectedDevice(server) } catch (e) { console.log(e) } finally { setIsLoading(false) } } if (isLoading) { return ( <div className='flex min-h-screen min-w-screen justify-center items-center'> <h2>Loading...</h2> </div> ) } return ( <div className='flex min-h-screen min-w-screen justify-center items-center'> {connectedDevice === null ? (<button onClick={connectToDevice} className='btn'>Connect</button>) : (<div className='text-2xl font-bold'>{temperature}</div>)} </div> ) }

Breaking down the React Web Bluetooth Code

Breaking down the code above we first declare some imports and the Bluetooth Service and Characteristic UUIDs:

import { useState, useEffect } from 'react' export const ADAFRUIT_TEMPERATURE_SERVICE_UUID = 'ADAF0100-C332-42A8-93BD-25E905756CB8'.toLowerCase() export const ADAFRUIT_TEMPERATURE_CHARACTERISTIC_UUID = 'ADAF0101-C332-42A8-93BD-25E905756CB8'.toLowerCase()

Next, we initialize some state:

const [supportsBle, setSupportsBle] = useState(false) const [connectedDevice, setConnectedDevice] = useState(null) const [isLoading, setIsLoading] = useState(false) const [temperature, setTemperature] = useState(0)

Check to make sure our browser supports Web Bluetooth

useEffect(() => { if (navigator.bluetooth) { setSupportsBle(true) } }, [])

Declare some callback functions to be used for any BLE updates

const onDisconnected = (event) => { setConnectedDevice(null) } const notifyEventChange = async (e) => { const val = e.currentTarget.value.getFloat32(0, true) setTemperature(val) }

Next, we have the function that kicks off the connection request.

const connectToDevice = async () => { if (!supportsBle) { return } try { const device = await navigator.bluetooth.requestDevice({ filters: [{ namePrefix: 'CIRCUITPY' }], optionalServices: [ADAFRUIT_TEMPERATURE_SERVICE_UUID] }) if (!device) { throw new Error() } device.addEventListener('gattserverdisconnected', onDisconnected) setIsLoading(true) const server = await device.gatt.connect() const temperatureService = await server.getPrimaryService(ADAFRUIT_TEMPERATURE_SERVICE_UUID) const tempCharacteristic = await temperatureService.getCharacteristic(ADAFRUIT_TEMPERATURE_CHARACTERISTIC_UUID) await tempCharacteristic.startNotifications() tempCharacteristic.addEventListener('characteristicvaluechanged', notifyEventChange) setConnectedDevice(server) } catch (e) { console.log(e) } finally { setIsLoading(false) } }

It will only run if our browser supports Bluetooth. As defined by the state we check once the component mounts in useEffect

if (!supportsBle) { return }

Now we try to connect to the device. Note that the Bluetooth wrapper needs to have filters defined. There are a few options

const device = await navigator.bluetooth.requestDevice({ filters: [{ namePrefix: 'CIRCUITPY' }], optionalServices: [ADAFRUIT_TEMPERATURE_SERVICE_UUID] }) if (!device) { throw new Error() }

Finally, connect to the device, get the temperature service and characteristic, and establish our callback listeners. We are subscribing to the events of the device.

device.addEventListener('gattserverdisconnected', onDisconnected) const server = await device.gatt.connect() const temperatureService = await server.getPrimaryService(ADAFRUIT_TEMPERATURE_SERVICE_UUID) const tempCharacteristic = await temperatureService.getCharacteristic(ADAFRUIT_TEMPERATURE_CHARACTERISTIC_UUID) await tempCharacteristic.startNotifications() tempCharacteristic.addEventListener('characteristicvaluechanged', notifyEventChange)

Then just wire up some simple jsx to display the state of our connection sequence.

if (isLoading) { return ( <div className='flex min-h-screen min-w-screen justify-center items-center'> <h2>Loading...</h2> </div> ) } return ( <div className='flex min-h-screen min-w-screen justify-center items-center'> {connectedDevice === null ? (<button onClick={connectToDevice} className='btn'>Connect</button>) : (<div className='text-2xl font-bold'>{temperature}</div>)} </div> )

If all is well, our React Bluetooth app should connect and display the CPU temperature as expected!

React Bluetooth Temperature Monitor

Feel free to reference the full code repository.