開発
Web Bluetooth API “Hello world!”
Mola BogdanGeorgii
Intro
Originally JavaScript (ECMAScript) has been known for manipulation with data and content strictly within context web browsers. For security reasons, it has been isolated from the operating system level. However, JavaScript has become applied in scopes that have gone beyond the borders of its original design (for example Node.js) and become more and more ubiquitous. Recently it met another example of well known and ubiquitous technology is Bluetooth. This communication standard continues to get new updates and used in many applications such as wireless audio devices, fitness metrics transmission, entertainment, and much more. Now it is possible for websites to communicate with BlueTooth devices. Isn’t it mindblowing? If you need to get familiar with this tech, let us help you to establish “zero ground”.
Before we start
Before your engineering imagination will carry you away, it is important to understand that this technology is still experimental and so far not compatible with all browsers and platforms. Given below code snippets have been tested with Windows 10 x64, Mac OS Mojave, and Android. The chrome browser has been used on every platform. In order to get more details please check out this table.
One more thing worth to be mentioned, Web Bluetooth API heavily uses async/await or Promises mechanism (introduced in the ES6 in 2015). If you are not familiar with it, it is highly recommended to check this and this articles.
Typical Web Bluetooth App workflow.
When we talk about WebBluetooth, it is necessary to understand basic terms. In particular technology itself is part of Bluetooth specification called Generic Attribute Profile, which you can usually meet as GATT.
In conventional BlueTooth workflow, we used to think about devices as central devices and peripherals. From GATT’s point we are dealing with clients and servers. Which is very convenient as we work in the field of web development. For example, your smart lamp can be a server and your smartphone can be a client. The smart lamp provides a service, i.e. light, light color, and so on. Similar to how a browser connects to a server on the Internet, your smartphone(or whatever with a compatible browser) is a client that connects to the GATT server in a smart lamp. Every service in a BlueTooth device is represented by a UUID hexadecimal value. In its turn service can have one or more characteristics that store actual values. Characteristics have UUID, user description (optional), and of course value. They also can have the following properties: reading, writing, writing without response, notification, and indication. In the example below reading will be focused. It is important to know that some services and characteristics are part of the standard (for example Heart Rate Measurement or Battery Level), but it is possible to define their own. For training purposes, it is convenient to generate UUID online.
Keep in mind that there is a blocklist that has been created to restrict the set of GATT attributes a website can access. This list is updated from time to time. Simply speaking this list aimed to eliminate security issues.
In general, the workflow is quite straightforward:
- Scanning device
- Connect to it
- Get the necessary Service
- Get the Characteristic/Characteristics
- Read, Write or Subscribe to the Characteristic
Connecting to BlueTooth service suppose some device. You can save time by using your smartphone as a virtual BlueTooth device. There are different applications that can serve this purpose but I preferred LightBlue®. There are iOS and Android versions.
Before start, it is necessary to prepare your virtual BlueTooth device. Open LightBlue and open the Virtual Devices tab. By touching the “Plus” sign on the left-right corner you will open a list of devices that are part of the protocol standard. You can add any and edit after according to your necessities.
By touching the device will open the properties screen. What is important here is to set up a name for a virtual device. See the picture below. It will simplify the search.
The next step is the definition of UUID for service. That UUID can be generated with the mentioned before website. The value of UUID will serve as a parameter for scanning. In general, the parameter objects for searching allow the use of a lot of options. It is possible to overwrite the default UUID of a virtual. And of course, it is possible to add characteristics like in the picture below:
Let’s scan first. I’m going to use async/await and chain of Promises to make everything (hopefully) easier to understand.
navigator.bluetooth.requestDevice( // 1st Promise requesting device
{ filters: [{services: [/* your device UUID */]}] })
.then(device => { // result of 1st Promise
console.log('Connecting to GATT Server...');
return device.gatt.connect(); // using result of 1st promise and
// making 2nd Promise to connect
})
.then(server => { // result of 2nd Promise
console.log('Getting *your device* Service...');
return server.getPrimaryService(/* your device UUID */); // using result of 2nd Promise
})
.then(service => { // and so on...
console.log('Getting custom characteristic ...');
extractData(service); // custom async function
})
.then(characteristic => {
console.log('Reading custom characteristic ...');
return characteristic.readValue();
})
.catch(error => {
console.log('Argh! ' + error); // gathering errors
});
An important caveat here. At some moment I used pseudocode to show the moment when you need (or not) to check your platform. During development, I encountered that Windows and Android accepting text values with so-called “null character” /u0000 that will bring a lot of headache during debugging because in the console you will see normal names and values but attempting to read your object will result in undefined. That’s why it is necessary to perform a few extra manipulations with value and clean up the string.
let extractData = async function(service) {
let resultObj = {};
try {
console.log('Getting Characteristics...');
const characteristics = await service.getCharacteristics();
for (const characteristic of characteristics) {
console.log('Getting Descriptors...');
try {
const descriptors = await characteristic.getDescriptors();
const CharacteristicName = await getCharacteristicName(descriptors); // Reading charasteristic name is a bit tricky so it is in a separeate method
const charValue = await characteristic.readValue();
const normalizedVal = handleRawValue(charValue); // Raw binary should be handled to get decimal value
// manipulate with your values
// For example create object.
resultObj[CharacteristicName] = normalizedVal;
}
catch(error) {
console.log('Error inside for: ', error);
}
}
}
catch(error) {
console.log('Error in reading Characteristics: ', error);
}
};
let getCharacteristicName = async function(descriptors) {
let charName = 'NA';
for (const descriptor of descriptors) {
// there can me other types of descriptor but for simplicity I left user description only.
switch (descriptor.uuid) {
case BluetoothUUID.getDescriptor('gatt.characteristic_user_description'):
try {
const value = await descriptor.readValue();
let decoder = new TextDecoder('utf-8');
charName = decoder.decode(value);
// =====
if (/* Check your platform here */) {
charName = decoder.decode(value); // tested on Mac OS
} else {
// tested on Windows 10 and Android
const decodedStr = `${decoder.decode(value)}`;
charName = decodedStr.substr(0, decodedStr.indexOf('\0'));
}
// =====
}
catch(error) {
console.log('Error in async method of descriptor.readValue(): ', error);
}
break;
default: console.log('> Unknown Descriptor: ' + descriptor.uuid);
}
}
return charName;
};
And finally a few words about the reading of values. The device provides binary data. It means that the data will come in an array where every index store a byte representation of the number. Let’s suppose that for characteristic “Number” the hexadecimal value 0xa7d8 has been set like in the picture below:
Under the value, you can see format. The same name has a button on the top-left corner. By touching the Characteristic format menu will be open.
At the bottom part pay attention to the endian system. In the picture below you can see how this option significantly changes the value (42968 and 55463).
More you can read about it there.
The code snippet below shows a simple method of iteration through byte array and making a new array of the hexadecimal string representation of bytes. Then it is nothing more than merging an array in one string and convert to an integer.
let handleRawValue = function(val) {
const parts = [];
for (let i = 0; i < val.byteLength; i += 1) {
parts.push(val.getUint8(i).toString(16));
}
const result = parseInt(parts.join(''), 16);
return result;
};
Web Bluetooth API is evolving and a huge topic. Depending on your goals you may need to get more deep knowledge about other aspects of this technology. Feel free to check out the link below.
Further reading:
- Mozzila documentation Web Bluetooth API
- Communicating with Bluetooth devices over JavaScript
- An Introduction To WebBluetooth
- Bluetooth over the Web