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

You may want to check out the final version of the app you're about to create at https://googlecodelabs.github.io/candle-bluetooth and play with the PLAYBULB Candle Bluetooth device you have at your disposal before actually diving into this codelab.

You can also watch me changing colors at https://www.youtube.com/watch?v=fBCPA9gIxlU

Download the sample code

You can get the sample code for this code by either downloading the zip here:

Download source code

or by cloning this git repo:

git clone https://github.com/googlecodelabs/candle-bluetooth.git

If you downloaded the source as a zip, unpacking it should give you a root folder candle-bluetooth-master.

Install and verify web server

While you're free to use your own web server, this codelab is designed to work well with the Chrome Web Server. If you don't have that app installed yet, you can install it from the Chrome Web Store.

Install Web Server for Chrome

After installing the Web Server for Chrome app, click on the Apps shortcut on the bookmarks bar:

In the ensuing window, click on the Web Server icon:

You'll see this dialog next, which allows you to configure your local web server:

Click the choose folder button, and select the root of the cloned (or unarchived) repo. This will enable you to serve your work in progress via the URL highlighted in the web server dialog (in the Web Server URL(s) section).

Under Options, check the box next to "Automatically show index.html", as shown below:

Now visit your site in your web browser (by clicking on the highlighted Web Server URL) and you should see a page that looks like this:

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 (port number by default is 8887). After that, you can simply open a new Chrome tab to http://localhost:8887 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, visit your site in your web browser (by clicking on the Web Server URL highlighted in the web server app) or simply refresh existing page. 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.remove('connecting');
    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():

playbulbCandle.js

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
    connect() {
      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
      return navigator.bluetooth.requestDevice(options)
      .then(device => {
        this.device = device;
        return device.gatt.connect();
      });
    }
  }

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 device.gatt.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.device.gatt.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.remove('connecting');
    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;
}

At this point, visit your site in your web browser (by clicking on the Web Server URL highlighted in the web server app) or simply refresh existing page. 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.device.gatt.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.remove('connecting');
    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 + '%';
}

At this point, visit your site in your web browser (by clicking on the Web Server URL highlighted in the web server app) or simply refresh existing page.. 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.device.gatt.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);
  }
}

At this point, visit your site in your web browser (by clicking on the Web Server URL highlighted in the web server app) or simply refresh existing page. 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.device.gatt.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.device.gatt.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;
  }
}

At this point, visit your site in your web browser (by clicking on the Web Server URL highlighted in the web server app) or simply refresh existing page. 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.

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.

Fix "no effect"

As you may have noticed, the "no effect" option doesn't reset any effect in progress, this is minor but still. Let's fix this. In the setColor method, you'll need to check first if an effect is in progress through a new class variable _isEffectSet and if true, turn off the effect before setting new color with these data: [0x00, r, g, b, 0x05, 0x00, 0x01, 0x00].

Write device name

This one is easy! Writing a custom device name is as simple as writing to the previous Bluetooth device name characteristic. I would recommend using TextEncoder encode method to get a Uint8Array containing the device name.

Then, I'd add an "input" eventListener to document.querySelector('#deviceName') and call playbulbCandle.setDeviceName to keep it simple.

I personally named mine PLAY💡 CANDLE!

What you've learned

Next Steps