Control bluetooth TOYS with your webpage

Szabolcs Damján
Byborg Engineering
Published in
6 min readJul 3, 2023

--

Are you planning to control Bluetooth devices from your web application?

Read on and learn how to:

  • connect to a device to send and receive data
  • implement an auto-connect feature to connect the device without any user interaction

The example application used in this article controls Bluetooth model cars that were part of a former Shell promotion.

The user interface mimics a touchpad, allowing simple touch gestures to drive and turn the car. The interface also displays on-screen data like motor power, battery level, and the name of the connected car.

Let’s break down how to do this in a few easy steps!

Handling BLE devices

Currently, Bluetooth communication can be can be split into two main categories: “Bluetooth classic,” which includes features like audio, and Bluetooth Low Energy (BLE). The latter is designed for low bit rate data exchanges with minimal power consumption.

The chrome browser implements only the BLE version.

To work with BLE devices, your application should:

  • Search for a Bluetooth device
  • Set up the connection
  • Send and retrieve data after a successful connection

It’s important to note that some BLE devices can automatically notify the host (your webpage) about data changes.

Search for a device

To discover Bluetooth devices, the operation must be initiated by the user. Fulfilling this requirement is easy. You should call your “connect” function from the callback handler of a user-initiated event (e.g., a click event).

// use try-catch, 
// because bluetooth is not implemented in every browser(version)
try {
device = await navigator.bluetooth.requestDevice({
// acceptAllDevices: true,
filters: [
{
namePrefix: "S",
},
// {
// services: [
// 0x180f,
// 0xfff0,
// ],
// },
],
optionalServices: [
0x180f, // battery service
0xfff0, // control service
],
});
} catch (error) {
console.log(`requestDevice error:`, error);
}

The Bluetooth discovery feature (requestDevice function) needs to be set up with the correct parameters. You can scan for all devices in range using the “acceptAllDevices” flag or use filters to narrow down the results.

It’s worth mentioning that in your device request, you should list the services you intend to use. These can be defined either as a filter or as an ‘optional service’. It is also possible to use the short form of the service IDs.

If you leave out the service IDs from both the “filters” and “optional services” section, the browser will refuse your service usage attempts!

Making a connection

After obtaining the required connection reference to the device, you can start implementing the process!

  1. Attach the handler for the disconnect event:
device.addEventListener("gattserverdisconnected", (event) => {
console.log("device disconnected:", event);
});

2. Establish connection to the GATT server:

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

console.log("gatt server:", server);

3. Preparing the characteristic IDs

For data exchange, you will need the characteristic IDs. To obtain these, start by querying the service IDs and then request the corresponding characteristic ID from these services.

const controlService = (await server.getPrimaryServices(0xfff0))[0];
const controlCharacteristic = (await controlService.getCharacteristics())[0];

const batteryService = (await server.getPrimaryServices(0x180f))[0];
const batteryCharacteristic = (await batteryService.getCharacteristics())[0];

Transferring data

Data exchange is performed by reading or writing the corresponding characteristic. Since the data transfer is in binary form, you receive or send “typed arrays”.

// reading battery level
const batteryLevel = await batteryCharacteristic.readValue();
console.log("battery level:", batteryLevel.getInt8(0));

// sending control data to the model car
const payload = new Uint8Array([
0x01,
0x00 /* drive forward*/,
0x00 /* drive reverse*/,
0x00 /* turn left */,
0x00 /* turn right */,
0x01 /* light up lamp */,
0x00 /* turn on turbo */,
]);
await controlCharacteristic.writeValue(payload);

As you can see the code snippet above, you can control the toy car by sending numerical encoded “flags” ( 0 — off, 1 — on ). This method allows you to turn the front wheels left or right and to run the motor forward or backward. Additional features include switchable front lights and a ‘turbo’ function for an extra power boost.

Listening for notifications

If you want to periodically get data from the device, you don’t need to poll the desired characteristic. Instead, you can register for a notification service, then the device will send the relevant data when necessary.

batteryNotification.addEventListener("characteristicvaluechanged", (event) => {
const { value } = event.target;
console.log("battery notification:", value.getInt8(0));
});

Auto connect possibilities

By implementing a BLE connection with the method above, the necessary scanning and device selection process could potentially become bothersome to the user.

Even though the user has already granted permission to the device by selecting it, the browser requires this action every time the application starts. This isn’t the most user-friendly experience…

To solve this issue, the new ‘permission-backend’ feature allows your webpage to connect to an already authorised device without any additional user interaction!

Sounds awesome! 🤩

However, as the time of writing this article, this feature is still behind a feature flag. To enable this promising feature, you must activate this flag in your browser settings.

chrome://flags/#enable-experimental-web-platform-features

site connecting to device automatically on startup

Implement auto connect

First, the site tries to query the known Bluetooth devices.

// auto connect 1. step
try {
devices = await navigator.bluetooth.getDevices();
console.log("devices:", devices);
} catch (error) {
// feature is probably not enabled
// try to enable chrome://flags/#enable-experimental-web-platform-features
console.log("error:", error);
}

In this context, “known devices” are the devices which have already been selected and paired by the user.

Once the known devices are retrieved, your application can display the names of these devices before making any connection.

Now that you have the reference for the device, attempting to connect may give you the following error:

DOMException: Bluetooth Device is no longer in range.

To avoid this issue, the site should subscribe to the device’s advertisement messages and must receive at least one message from the device. After successfully receiving this data packet, the standard connection method can be used.

// step 2. prepare connection by waiting for the first advertisement message
const savedDevice = devices[0];

const waitForAdvertisement = () =>
new Promise((resolve, reject) => {
savedDevice.onadvertisementreceived = (event) => {
console.log(`advertisement received:`, event);
resolve(event);
};
});

const signal = new AbortController();
// start listening to advertisements
await savedDevice
.watchAdvertisements({ signal: signal.signal })
.catch((error) => {
console.log(`watch advertisements error:`, error);
});

// the "delay" function is a simple setTimeout wrapped into a Promise
const result = await Promise.race([waitForAdvertisement(), delay(4000)]);
console.log(`received advertisement:`, result);
// after receiving the message or in case of timeout
// we should stop listening to advertisements
signal.abort();

Conclusion

While the basic BLE connection and data exchange features are mature and production-ready in Chrome, advanced features like advertisement processing or auto connect are still behind a feature flag and not enabled by default in the client’s browser. Hopefully they will be available in production soon! 🤞

Resources

Some inspiration was taken from this repository:

--

--