In this enlightened codelab, you'll learn how to control a PLAYBULB LED flameless candle with nothing but JavaScript thanks to the experimental Web Bluetooth API. Along the way, you'll also play with new JavaScript ES2015 features such as Classes, Arrow functions, Map, and Promises.
The first time you run Chrome Dev Editor it will ask you to set up your workspace environment.
Fire up Chrome Dev Editor and clone the candle-bluetooth GitHub repository:
At any point, select the index.html file and hit the button in the top toolbar to run the app. Chrome Dev Editor will fire up a local web server and navigate to the
index.html
page. This is great way to preview changes as you make them.
If you want to see what this app looks like on your Android phone, you'll need to enable Remote debugging on Android and set up Port forwarding with port number 31999. After that, you can simply open a new Chrome tab to http://localhost:31999/candle-bluetooth/index.html on your Android phone.
At this point this web app doesn't do much. Let's start adding Bluetooth support!
We'll start by writing a library that uses a JavaScript ES2015 Class for the PLAYBULB Candle Bluetooth device.
Keep calm. The class syntax is not introducing a new object-oriented inheritance model to JavaScript. It simply provides a much clearer syntax to create objects and deal with inheritance, as you can read below.
First, let's define a PlaybulbCandle
class in playbulbCandle.js
and create a playbulbCandle
instance that will be available in the app.js
file later.
(function() {
'use strict';
class PlaybulbCandle {
constructor() {
this.device = null;
}
}
window.playbulbCandle = new PlaybulbCandle();
})();
To request access to a nearby Bluetooth device, we need to call navigator.bluetooth.requestDevice
. Since the PLAYBULB Candle device advertises continuously (if not paired already) a constant Bluetooth GATT Service UUID known in its short form as 0xFF02
, we can simply define a constant and add this to the filters services parameter in a new public connect
method of the PlaybulbCandle
class.
We will also keep track internally of the BluetoothDevice
object so that we can access it later if needed. Since navigator.bluetooth.requestDevice
returns a JavaScript ES2015 Promise, we'll do this in the then
method.
(function() {
'use strict';
const CANDLE_SERVICE_UUID = 0xFF02;
class PlaybulbCandle {
constructor() {
this.device = null;
}
connect() {
let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
return navigator.bluetooth.requestDevice(options)
.then(function(device) {
this.device = device;
}.bind(this));
}
}
window.playbulbCandle = new PlaybulbCandle();
})();
As a security feature, discovering nearby Bluetooth devices with navigator.bluetooth.requestDevice
must be called via a user gesture like a touch or mouse click. That's why we'll call the connect
method when user clicks the "Connect" button in the app.js
file:
document.querySelector('#connect').addEventListener('click', function(event) {
document.querySelector('#state').classList.add('connecting');
playbulbCandle.connect()
.then(function() {
console.log(playbulbCandle.device);
document.querySelector('#state').classList.remove('connecting');
document.querySelector('#state').classList.add('connected');
})
.catch(function(error) {
console.error('Argh!', error);
});
});
At this point, select the index.html
file and hit the button in the top left corner to run the app. Click the green "Connect" button, pick the device in the chooser and open your favorite Dev Tools console with Ctrl + Shift + J keyboard shortcut and notice the
BluetoothDevice
object logged.
You might get an error if Bluetooth is off and/or the PLAYBULB Candle bluetooth device is off. In that case, turn it on and proceed again.
I don't know about you but I already see too many function() {}
in this code. Let's switch to () => {}
JavaScript ES2015 Arrow Functions instead. They are absolute life savers: All the loveliness of anonymous functions, none of the sadness of binding.
(function() {
'use strict';
const CANDLE_SERVICE_UUID = 0xFF02;
class PlaybulbCandle {
constructor() {
this.device = null;
}
connect() {
let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
return navigator.bluetooth.requestDevice(options)
.then(device => {
this.device = device;
});
}
}
window.playbulbCandle = new PlaybulbCandle();
})();
document.querySelector('#connect').addEventListener('click', event => {
playbulbCandle.connect()
.then(() => {
console.log(playbulbCandle.device);
document.querySelector('#state').classList.add('connected');
})
.catch(error => {
console.error('Argh!', error);
});
});
- OK... can I actually talk to this candle or what?
- Sure... jump to the next step
So what do you do now that you have a BluetoothDevice
returned from navigator.bluetooth.requestDevice
's promise? Let's connect to the Bluetooth remote GATT Server that holds the Bluetooth service and characteristic definitions by calling device.gatt.connect()
and keep track of it internally:
class PlaybulbCandle {
constructor() {
this.device = null;
this.server = null;
}
connect() {
let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
return navigator.bluetooth.requestDevice(options)
.then(device => {
this.device = device;
return device.gatt.connect();
})
.then(server => {
this.server = server;
});
}
}
Here we are connected to the GATT Server of the PLAYBULB Candle Bluetooth device. Now we want to get the Primary GATT Service (advertised as 0xFF02
previously) and read the device name characteristic (0xFFFF
) that belongs to this service. This can be easily achieved by adding a new method getDeviceName
to the PlaybulbCandle
class and using server.getPrimaryService
and service.getCharacteristic
. The characteristic.readValue
method will actually return a DataView
we'll simply decode with TextDecoder
.
const CANDLE_DEVICE_NAME_UUID = 0xFFFF;
...
getDeviceName() {
return this.server.getPrimaryService(CANDLE_SERVICE_UUID)
.then(service => service.getCharacteristic(CANDLE_DEVICE_NAME_UUID))
.then(characteristic => characteristic.readValue())
.then(data => {
let decoder = new TextDecoder('utf-8');
return decoder.decode(data);
});
}
Let's add this into app.js
by calling playbulbCandle.getDeviceName
once we're connected and display the device name.
document.querySelector('#connect').addEventListener('click', event => {
playbulbCandle.connect()
.then(() => {
console.log(playbulbCandle.device);
document.querySelector('#state').classList.add('connected');
return playbulbCandle.getDeviceName().then(handleDeviceName);
})
.catch(error => {
console.error('Argh!', error);
});
});
function handleDeviceName(deviceName) {
document.querySelector('#deviceName').value = deviceName;
}
Select the index.html
file and hit the button in the top left corner to run the app as seen before. Make sure the PLAYBULB Candle is turned on, then click the "Connect" button on the page and you should see the device name below the color picker.
There is also a standard battery level Bluetooth characteristic available in the PLAYBULB Candle Bluetooth device that contains the battery level of the device. This means we can use standard names such as battery_service
for the Bluetooth GATT Service UUID and battery_level
for the Bluetooth GATT Characteristic UUID.
Let's add a new getBatteryLevel
method to the PlaybulbCandle
class and read the battery level in percent.
getBatteryLevel() {
return this.server.getPrimaryService('battery_service')
.then(service => service.getCharacteristic('battery_level'))
.then(characteristic => characteristic.readValue())
.then(data => data.getUint8(0));
}
We also need to update the options
JavaScript object to include the battery service to the optionalServices
key as it is not advertised by the PLAYBULB Candle Bluetooth device but still mandatory to access it.
let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}],
optionalServices: ['battery_service']};
return navigator.bluetooth.requestDevice(options)
As before, let's plug this into app.js
by calling playbulbCandle.getBatteryLevel
once we have the device name and display the battery level.
document.querySelector('#connect').addEventListener('click', event => {
playbulbCandle.connect()
.then(() => {
console.log(playbulbCandle.device);
document.querySelector('#state').classList.add('connected');
return playbulbCandle.getDeviceName().then(handleDeviceName)
.then(() => playbulbCandle.getBatteryLevel().then(handleBatteryLevel));
})
.catch(error => {
console.error('Argh!', error);
});
});
function handleDeviceName(deviceName) {
document.querySelector('#deviceName').value = deviceName;
}
function handleBatteryLevel(batteryLevel) {
document.querySelector('#batteryLevel').textContent = batteryLevel + '%';
}
Select the index.html
file and hit the button in the top left corner to run the app. Click the "Connect" button on the page and you should see both device name and battery level.
- How can I change the color of this bulb? That's why I'm here!
- You're so close I promise...
Changing the color is as easy as writing a specific set of commands to a Bluetooth Characteristic (0xFFFC
) in the Primary GATT Service advertised as 0xFF02
. For instance, turning your PLAYBULB Candle to red would be writing an array of 8-bit unsigned integers equal to [0x00, 255, 0, 0]
where 0x00
is the white saturation and 255, 0, 0
are, respectively, the red, green, and blue values .
We'll use characteristic.writeValue
to actually write some data to the Bluetooth characteristic in the new setColor
public method of the PlaybulbCandle
class. And we will also return the actual red, green, and blue values when the promise is fulfilled so that we can use them in app.js
later:
const CANDLE_COLOR_UUID = 0xFFFC;
...
setColor(r, g, b) {
let data = new Uint8Array([0x00, r, g, b]);
return this.server.getPrimaryService(CANDLE_SERVICE_UUID)
.then(service => service.getCharacteristic(CANDLE_COLOR_UUID))
.then(characteristic => characteristic.writeValue(data))
.then(() => [r,g,b]);
}
Let's update the changeColor
function in app.js
to call playbulbCandle.setColor
when the "No Effect" radio button is checked. The global r, g, b
color variables are already set when user clicks the color picker canvas.
function changeColor() {
var effect = document.querySelector('[name="effectSwitch"]:checked').id;
if (effect === 'noEffect') {
playbulbCandle.setColor(r, g, b).then(onColorChanged);
}
}
Select the index.html
file and hit the button in the top left corner to run the app. Click the "Connect" button on the page and click the color picker to change the color of your PLAYBULB Candle as many times as you want.
If you've already lighted a candle before, you know the light isn't static. Luckily for us, there's another Bluetooth characteristic (0xFFFB
) in the Primary GATT Service advertised as 0xFF02
that lets the user set some candle effects.
Setting a "candle effect" for instance can be achieved by writing [0x00, r, g, b, 0x04, 0x00, 0x01, 0x00]
. And you can also set the "flashing effect" with [0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00]
.
Let's add the setCandleEffectColor
and setFlashingColor
methods to the PlaybulbCandle
class.
const CANDLE_EFFECT_UUID = 0xFFFB;
...
setCandleEffectColor(r, g, b) {
let data = new Uint8Array([0x00, r, g, b, 0x04, 0x00, 0x01, 0x00]);
return this.server.getPrimaryService(CANDLE_SERVICE_UUID)
.then(service => service.getCharacteristic(CANDLE_EFFECT_UUID))
.then(characteristic => characteristic.writeValue(data))
.then(() => [r,g,b]);
}
setFlashingColor(r, g, b) {
let data = new Uint8Array([0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00]);
return this.server.getPrimaryService(CANDLE_SERVICE_UUID)
.then(service => service.getCharacteristic(CANDLE_EFFECT_UUID))
.then(characteristic => characteristic.writeValue(data))
.then(() => [r,g,b]);
}
And let's update the changeColor
function in app.js
to call playbulbCandle.setCandleEffectColor
when the "Candle Effect" radio button is checked and playbulbCandle.setFlashingColor
when the "Flashing" radio button is checked. This time, we'll use switch
if that's OK with you.
function changeColor() {
var effect = document.querySelector('[name="effectSwitch"]:checked').id;
switch(effect) {
case 'noEffect':
playbulbCandle.setColor(r, g, b).then(onColorChanged);
break;
case 'candleEffect':
playbulbCandle.setCandleEffectColor(r, g, b).then(onColorChanged);
break;
case 'flashing':
playbulbCandle.setFlashingColor(r, g, b).then(onColorChanged);
break;
}
}
Select the index.html
file and hit the button in the top left corner to run the app. Click the "Connect" button on the page and play with the Candle and Flashing Effects.
- That's all? 3 poor candle effects? Is this why I'm here?
- There are more but you'll be on your own this time.
So here we are! You might think it's almost the end but the app is not over yet. Let's see if you actually understood what you've copy-pasted during this codelab. Here's what you want to do by yourself now to make this app shine:
Here are the data for the missing effects:
[0x00, r, g, b, 0x01, 0x00, 0x09, 0x00]
(you might want to adjust r, g, b
values there)[0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00]
(Epileptic people may want to avoid this one)[0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x26, 0x00]
This basically means adding new setPulseColor
, setRainbow
and setRainbowFade
methods to PlaybulbCandle
class and calling them in changeColor
.
As you may have noticed already, our code doesn't respect the don't repeat yourself (DRY) principle. So let's fix that!
First, create some util functions in the PlayBulbCandle
class to cache all bluetooth characteristics when the device is first connected. Then reuse these characteristics each time we want to write or read to a characteristic to make it faster.
Save all cached characteristics into a JavaScript ES2015 Map
in the PlayBulbCandle
class using a new private _cacheCharacteristic
method that takes a Bluetooth GATT Service and Bluetooth GATT Characteristic UUID as parameters.
Then, create a _readCharacteristicValue
new private method that only takes a Bluetooth GATT Characteristic UUID as a parameter and retrieves a cached characteristic to read the value. The same thing would apply to a new _writeCharacteristicValue
private method to write a characteristic value.
Finally, rewrite all your read/write public methods to take advantage of those two private methods.