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.

What you'll learn

What you'll need

How would rate your experience with Bluetooth?

Novice Intermediate Advanced

Clone the codelab project

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:

  1. Click the icon and select "Git Clone..."
  2. Enter "https://github.com/googlecodelabs/candle-bluetooth" as the Repository URL.
  3. Click the CLONE button.

Preview the app

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.

Next up

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.

playbulbCandle.js

(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.

playbulbCandle.js

(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:

app.js

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);
  });
});

Run the app

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.

Mandatory Bonus

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.

playbulbCandle.js

(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();

})();

app.js

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);
  });
});

Next up

- OK... can I actually talk to this candle or what?

- Sure... jump to the next step

Frequently Asked Questions

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:

playbulbCandle.js

  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;
      });
    }
  }

Read the device name

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.

playbulbCandle.js

  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.

app.js

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.

Read the battery level

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.

playbulbCandle.js

    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.

playbulbCandle.js

      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.

app.js

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.

Next up

- How can I change the color of this bulb? That's why I'm here!

- You're so close I promise...

Frequently Asked Questions

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:

playbulbCandle.js

  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.

app.js

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.

Moar candle effects

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.

playbulbCandle.js

  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.

app.js

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.

Next up

- 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:

  1. Add some missing effects
  2. Set a cool name for this device
  3. Apply DRY principle to our code

Add missing effects

Here are the data for the missing effects:

This basically means adding new setPulseColor, setRainbow and setRainbowFade methods to PlaybulbCandle class and calling them in changeColor.

Refactoring

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.

What you've learned

Next Steps