Some time ago I assembled unpretentious robot arm for fun and decided to control it with smartphone using Bluetooth. At that time I haven’t experience in native application development yet and I was already familiar with Apache Cordova, but I thought it would be interesting to use Web Bluetooth API, flavoring it by Progressive Web Apps features.
At first glance it may seem there are enough of articles for the keywords: there is a Web Bluetooth specification, a detailed article in the Google Developer blog providing examples, there is a thorough analysis (in Russian) of Bluetooth Low Energy, examples of reverse engineering of the protocols for various BLE devices and even of blinking “smart” bulbs and getting data from fitness wristbands right from the browser — what can go wrong?
I supposed so until I tried to build my own device and to chat with it from the browser. I will not dive deeply, just share my hands-on experience and code, since it would be very useful to me when I started :)
So you built a device, decided to equip it with Bluetooth module and to get access to it from the browser. Hereinafter there will be a picture of wiring a Bluetooth module to Arduino Uno, so let it be a device based on Arduino, although, of course, there is no fundamental difference whether you use STM, Raspberry, ESP8266 or anything else. The one thing important is that your controller will work with the Bluetooth module using the UART protocol (read more on Wikipedia).
If you, just like me, have already mounted the good old Bluetooth module HC-05 to your device, uploaded a firmware, launched some of Google examples and can’t understand the browser doesn’t detect your device, then I’m sorry to disappoint you: Web Bluetooth supports only the “Bluetooth 4 standard”.
This is the reason to write this article, because when you, as well as I, will return satisfied from the nearest store with the BLE module blazing (HM-10, for example), you will find that it works quite differently and, what is more important, not supports the Serial Port Profile, by which you are used to carelessly drive bytes back and forth.
Probably you’re familiar with Bluetooth Low Energy concept already, especially with Generic Attribute Profile (GATT), but I’ll try to explain shortly what is important for us now: instead of DIY serial protocol, your device should expose a set of applied “characteristics”, which can be read and/or change by connected device.
Robotic arm for example: it moves in space by three coordinates (X, Y, Z numbers), it can open (0) or close (1) a claw. It means we should configure BLE module to read and write 4 characteristics, which can be discovered, read and overridden with desired values by a connected device.
And it’s great, but here’s the thing: ordinary “hobby” level BLE modules that you’ll meet in the “next store” or on Aliexpress: HM-10, JDY-08, AT-09, some CC41-A that I got, or others — don’t have the ability to configure any services or characteristics.
Instead, they provide only one characteristic that emulates a serial port, and everything you write down there, module will send to your controller via TX, and everything you send from the controller to the module RX, it will send to the connected device. BTW, with a limit of 20 bytes, inherent in any BLE characteristic, by the way.
Thus, despite the fact that Web Bluetooth is limited to using the profile of common attributes, we will actually have to do a serial port profile on top of it for “domestic” use.
Bluetooth Low Energy module configuration
First of all, let’s configure the BLE module, it doesn’t take much time, if you know what to do. It turned out that I had a CC41-A module on the Texas Instruments CC2541 chip, which cost me around 5$ in the “next store”. So an example, I will describe its configuration, but the essence is common for other modules using a similar chip.
If you have a USB-TTL converter, just connect the BLE module to it and you will have direct access to the module from your computer via the COM port. Pay attention to the description of your module, maybe it works with 3.3V logic, in this case you will need to use a voltage level shifter for TX-RX and RX-TX lines. The CC41-A module, despite the fact that “LEVEL:3.3V” is written on it, handles 5V logic.
Instead of using the converter, you can take your controller, implementing the simplest serial bridge: everything you send to one serial port will be transfered to another, and vice versa. In the case of Arduino Uno, you will need to use the SoftwareSerial library:
Start the terminal program (you can use the Serial Monitor from Arduino IDE, I prefer Bray’s Terminal) and connect to the COM port on which the BLE module hangs with the standard settings:
- Baud rate: 9600
- Data bits: 8
- Parity: none
- Stop bits: 1
- Handshaking: none
In standby mode, the module responds to AT commands ending with carriage return and line feed (
CR+LF, the “Both NL & CR” option in the Serial Monitor). Some BLE modules work at a different speed by default, for example, at 38400, some modules enter into the configuration mode after pressing a button located on their board, some modules do not require commands to be in uppercase — check your module specifications.
Let’s send the “AT” command to check the connection. The module should respond “OK” — it means everything is OK. In fact, it’s enough to make sure that the module works in the slave mode, waiting for the connection of the master, the UUID of the service is
0xFFE0, and the UUID of the characteristic is set to
0xFFE1 — it will be needed later. Here are some commands that work with my module:
AT— check operability;
AT+HELP— output all commands;
AT+DEFAULT— reset factory settings;
AT+RESET— soft reset;
AT+ROLE— output current mode;
AT+ROLE0— set slave mode;
AT+NAME— output module name;
AT+NAMESimon— set module name as
AT+PIN— output PIN code (password) for pairing;
AT+PIN123456—set PIN code as
AT+UUID— output service UUID;
AT+UUID0xFFE0— set service UUID as
AT+CHAR— output characteristic UUD;
AT+CHAR0xFFE1— set characteristic UUID as
Now you can try to connect to the BLE module, for example from the Characteristic Properties Sample page, specifying “0xFFE0” as a service and “0xFFE1” as a characteristic. Or even to send something from the terminal to the browser using the Notifications Sample page.
Building a web app
Warm up is over, now to the interesting part!
Let’s consider the concept of your device control. You create a UI with a variety of controls that will interact with your device building an ordinary HTML page in the browser.
If it’s a robotic arm moving in three coordinates and opening and closing the claw, then it can be three numerical sliders or even a 2D surface, when you click on which the values of X and Y are calculated, one slider for movement along the Z axis and a button, opening or closing the claw. If it’s a kettle, you can make a button “Boil it!”. If it’s a RC car, you can make the “forward”, “backward”, “leftward”, “rightward”, “turn on / off the headlights”, “give a signal” buttons, etc.
For example, when you press the claw closure button, the
onclick button handler is triggered, which sends the
GRIPPER=CLOSE message. The controller receives a message, understands what it’s required to do, closes the claw and sends back the
GRIPPER=CLOSED message. Processing this message, we remember the state of the claw in JS and change the text on the button to “Open”.
Also we’ll implement the logging of the connecting to Bluetooth Low Energy device process, reconnection in case of break of communication and bypass the 20 bytes limitation of the BLE characteristic length.
Having ability to exchange messages between an HTML page and your device, a stable connection and a simple API, it’s easy to sharpen the application for your needs.
Appart of your favorite IDE, you will need a working “device”, which we configured earlier, it’ll help to receive and send messages in real time through the terminal program on the computer for application testing.
Web Bluetooth API is available by default in Chrome 56+ and Opera 43+. The Google Developers article also mentions that in Linux you need to enable
chrome://flags/#enable-experimental-web-platform-features flag and restart the browser.
And the last important point: a web app should be served using HTTPS (you can use GitHub Pages) or from the http://localhost — these are security requirements.
App will consist of one
index.html page, one
styles.css and one
main.js file, the latter will contain all the magic.
Let’s make a button to connect to the device, a button to disconect, div-container for messages and a form with a text field and a “Send” button:
Div-container will log connection process, output incoming and outcoming messages in the follow manner:
In order not to guess messages source, let’s divide them by colors:
As you can see, nothing special. The interface is ready :)
Further work will occur in the
Let’s get references to UI elements, set up handlers on the connect and disconnect buttons click and on the form submit:
Connecting to the device
Complete connection algorithm consists of several steps:
- Bluetooth device request: the browser starts the search of the nearest device and choose dialog, the user makes a choice, the app code gets the object.
- Connecting to the selected device fom the app code:
a) connection to the GATT server,
b) getting the desired service,
c) getting the desired characteristic.
- Enabling the characteristic changes notification — it’s necessary to receive messages from your device.
Arrange in the code:
So we implemented Promises chain representing connection steps in
We also defined the
deviceCache variable, which will store object of the device selected by the user in order to reconnect to in case of disconnection.
The first line of the
connect() function body: if the
deviceCache object is nonzero the ternary operator returns a Promise immediately fulfilled, otherwise it accesses to the Bluetooth device request function. Thus, when the “Connect” button is pressed if the user has already connected to the device, the device selection dialog does not appear.
We output error to the terminal with the help of
log() function, if it occurs at any step. This function will be implemented later.
Bluetooth device request
In order to request Bluetooth device selection, it’s needed to invoke
navigator.bluetooth.requestDevice() with object-configuration as required argument, which describes in what type of Bluetooth devices we interested in. It’s possible to filter by service, by name, it’s possible to request all types of devices, however we still have to specify service we want to work with, otherwise browser will not allow us to access to it.
We are requesting all devices that provide a service with
0xFFE0 UUID, the same as configured in the BLE module. Promise fulfilled with the selected device object, so we cache it and return.
Connecting to the device, getting service and characteristic objects
We perform a simple Promise chain, which speaks for itself. The
characteristicCache variable — similar to
deviceCache — saves the received characteristic object, it will be required to write data to it, in other words to send a message from the browser to the device.
UUIDs configured in the BLE module used as an argument n
Enabling the characteristic changes notification
It is enough to call the
startNotifications() method of the characteristic object, and listen for the characteristic change event after, but more on that later.
Output to the terminal
Let’s implement a function to output to the terminal, to test the connection to the device right now:
We insert a div with the class specified in the
type argument to the end of the terminal div-container using the
insertAdjacentHTML() method — easy peasy.
Open the page in the browser, click the “Connect” button, and then the device selection dialog will start. Connect to your device, and the terminal will output messages about the connection process.
Here I run into a bluetooth connection problem that wasn’t described in the Internet. Service messages output helped diagnose and fix it. The connection can suddenly be lost for no apparent reason, and the browser does not provide its maintenance, that’s why we need to implement the reconnection functionality on our own.
Web Bluetooth offers a
gattserverdisconnected event for the device object to track the disconnection. The most actual place to define a listener is in the device selection function:
Now, if you connect to the device and then disconnect it, and the Bluetooth connection will be lost, the browser will try to reconnect once:
Disconnecting from the device
It’s important not to forget to remove the assigned handler for the
gattserverdisconnected event, otherwise the browser will reconnect:
You can not reset the
deviceCache, then when you click the “Connect” button, the device selection dialog will not appear, connecting to the previous device instead.
Data is received from the device asynchronously using the notification mechanism that occurs when the value of the BLE characteristic changes. We only need to subscribe to the corresponding
characteristicvaluechanged characteristic event. Do this right after notifications turning on. It is also correct to remove the handler from the characteristic when the device is disconnected:
Testing shows that the messages receiving from the device works stably, with
LF end of line or without them. Long messages are received completely, but are split by 20 bytes.
Introduction of the intermediate buffer
You may not need to support messages longer than 20 bytes, but for the sake of completeness, let’s go around this limitation. The idea is simple: we’ll write the incoming strings into the intermediate buffer until we’ll get the delimiter character. When the delimiter character is received, we call the third function, passing data from the buffer, and clear the buffer for the subsequent writing.
It’s logical to make the line feed (
\n) the delimiter character. It may also be helpful to trim the message:
When creating a web app specific for your device, you can change the
receive() function to your own needs, being sure that you are working with the whole message from the device.
Sending data to the device is done by writing a value to the characteristic, and more specifically by calling the
writeValue() method of the characteristic object with
ArrayBuffer as its argument. To convert a string into
ArrayBuffer, it’s easiest to use
Just in case, the data is converted to a string type using the global
In this implementation, there is the same 20 bytes limitation: anything that goes beyond is simply truncated. Therefore, in case the message is longer than 20 bytes, it’s worth to break it into chunks and send it in sequence with some delay:
To make it easier to process messages on the controller side, add a line feed character (
\n) to the end of the message.
The message is split into chunks using a regular expression that correctly handles the carriage return (
\r) and line feed (
\n) characters. After that the first part is sent immediately, and timeouts are set to send other chunks with a delay of 100 milliseconds.
It woks! So we got a full-featured two-way data exchange with the device and at this point we are done with JS.
Progressive Web App
We don’t know in what conditions your device will be in advance, so it would be nice to be able to work with the created web app offline. And here the concept of Progressive Web Apps (more details on Google Developers or on Wikipedia) will help us: in a nutshell these are websites that look like ordinary or mobile apps for the user. With the use of PWA technologies at the first time you visit the website, you’ll be able to install it as an application on your smartphone’s homescreen and work with it offline.
The icon is required to install the application to the homescreen. I use realfavicongenerator.net — upload suitable image and the generator will prompt you to adjust the icons for different devices.
In the “Favicon for Android Chrome” section, I recommend to switch to the “Assets” tab and select “Create all documented icons”, otherwise Chrome will generate an icon for the homescreen from the size closest to the desired size, which can vary on different devices.
After finishing the configuration, press the “Generate” button, download the “Favicon package” and unpack it next to the web page. Also copy the code proposed by the generator to the
Together with the icons, the generator has kindly provided us with a manifest boilerplate —
Specify the name of your application in the
name property and add the
short_name property containing the abbreviated name, no more than 12 characters.
All the generated icons are already listed in the
icons array, the app display mode in the
standalone means that the web application will be launched without the browser UI elements, which is the most similar to the native application — exactly what is needed.
The browser will color its toolbar in the
theme_color color, and
background_color will be used as a background for the Splash screen shown on the app launch. Changing the
theme_color in the manifest, do not forget to change the meta tag
<meta name=”theme-color” content=“#ffffff”> also.
It’s also worth to define the
scope properties equal to
./, to open main page on the application launch firstly, and to constrain the navigation area of the web app with the current and nested pages secondly. The latter can be useful if the application files aren’t located in the root directory of the website.
Service Worker will allow us to cache files necessary for the app and use them offline. Service Worker Toolbox will help us to quickly create our own, just download sw-toolbox.js and companion.js files, place them next to
index.html and add the following to the
The last thing is to add the
sw.js script next to
index.html to cache necessary files:
Now we have not just a page, but a real progressive web application:
So that’s how easy and simple to start developing your own Bluetooth Low Energy devices with cheap modules and cross-platform web apps, using a two-way communication channel between the device and the browser.
Now you can change the UI to the specific for your device, add necessary handlers, implement your protocol of communication with the device and… turn on a kettle in the kitchen by pressing button in the app on your smartphone!
Final code of
sw.js files available here.