使用网络蓝牙控制 PLAYBULB 蜡烛

1. 这是什么?

IMG_19700101_023537~2~2.jpg

在这个富有启发性的 Codelab 中,您将学习如何借助 Web Bluetooth API,仅使用 JavaScript 控制 PLAYBULB LED 无焰蜡烛。在此过程中,您还将体验 JavaScript ES2015 的各种功能,例如类、箭头函数、映射和 Promise。

学习内容

  • 如何在 JavaScript 中与附近的蓝牙设备互动
  • 如何使用 ES2015 类、箭头函数、Map 和 Promise

所需条件

2. 优先播放

在实际开始此 Codelab 之前,您可能需要访问 https://googlecodelabs.github.io/candle-bluetooth,查看您即将创建的应用的最终版本,并试用您手头的 PLAYBULB Candle 蓝牙设备。

您还可以通过以下网址观看我变色的视频:https://www.youtube.com/watch?v=fBCPA9gIxlU

3. 进行设置

下载示例代码

您可以通过以下方式获取此代码的示例代码:在此处下载 ZIP 文件:

或克隆此 Git 代码库:

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

如果您下载的是 ZIP 格式的源代码,解压缩后应该会得到一个根文件夹 candle-bluetooth-master。

安装并验证 Web 服务器

虽然您可以随意使用自己的网络服务器,但此 Codelab 旨在与 Chrome Web Server 搭配使用。如果您尚未安装该应用,则可以通过 Chrome 应用商店安装。

安装 Web Server for Chrome 应用后,点击书签栏上的“应用”快捷方式:

Screen Shot 2016-11-16 at 4.10.42 PM.png

在随后显示的窗口中,点击“Web Server”图标:

9f3c21b2cf6cbfb5.png

接下来,您将看到以下对话框,该对话框可让您配置本地网络服务器:

Screen Shot 2016-11-16 at 3.40.47 PM.png

点击选择文件夹按钮,然后选择克隆(或解压缩)的代码库的根目录。这样您就可以通过网络服务器对话框中(Web Server URL(s) [网络服务器网址] 部分)突出显示的网址处理正在进行的工作。

在“选项”下,选中“自动显示 index.html”旁边的复选框,如下所示:

Screen Shot 2016-11-16 at 3.40.56 PM.png

现在,在网络浏览器中访问您的网站(点击突出显示的 Web 服务器网址),您应该会看到如下所示的页面:

Screen Shot 2016-11-16 at 3.20.22 PM.png

如果您想查看此应用在 Android 手机上的显示效果,则需要启用 Android 远程调试并设置端口转发(默认端口号为 8887)。之后,您只需在 Android 手机上打开一个新的 Chrome 标签页,然后访问 http://localhost:8887 即可。

后续步骤

此时,此 Web 应用的功能还不多。我们开始添加蓝牙支持吧!

4. 探索蜡烛

首先,我们将编写一个库,该库使用 JavaScript ES2015 类来表示 PLAYBULB Candle 蓝牙设备。

保持冷静。类语法不会向 JavaScript 引入新的面向对象的继承模型。它只是提供了一种更清晰的语法来创建对象和处理继承,如下所述。

首先,我们在 playbulbCandle.js 中定义一个 PlaybulbCandle 类,并创建一个 playbulbCandle 实例,该实例稍后将在 app.js 文件中可用。

playbulbCandle.js

(function() {
  'use strict';

  class PlaybulbCandle {
    constructor() {
      this.device = null;
    }
  }

  window.playbulbCandle = new PlaybulbCandle();

})();

如需请求访问附近的蓝牙设备,我们需要调用 navigator.bluetooth.requestDevice。由于 PLAYBULB Candle 设备会持续通告(如果尚未配对)一个简称为 0xFF02 的常量蓝牙 GATT 服务 UUID,因此我们只需定义一个常量,并将其添加到 PlaybulbCandle 类的新公共 connect 方法中的过滤条件服务参数。

我们还会在内部跟踪 BluetoothDevice 对象,以便日后需要时可以访问它。由于 navigator.bluetooth.requestDevice 会返回 JavaScript ES2015 Promise,因此我们将在 then 方法中执行此操作。

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

})();

作为一项安全功能,使用 navigator.bluetooth.requestDevice 发现附近的蓝牙设备必须通过用户手势(例如轻触或点击鼠标)来调用。因此,当用户点击 app.js 文件中的“Connect”按钮时,我们将调用 connect 方法:

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

运行应用

此时,请在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或直接刷新现有页面。点击绿色的“Connect”按钮,在选择器中选择设备,然后使用 Ctrl + Shift + J 键盘快捷键打开您常用的开发者工具控制台,并注意记录的 BluetoothDevice 对象。

Screen Shot 2016-11-16 at 3.27.12 PM.png

如果蓝牙和/或 PLAYBULB Candle 蓝牙设备处于关闭状态,您可能会收到错误消息。在这种情况下,请将其开启,然后重试。

强制性奖金

我不知道您怎么看,但我已经在这段代码中看到了太多 function() {}。我们改为使用 () => {} JavaScript ES2015 箭头函数。它们绝对是救星:既有匿名函数的所有优点,又没有绑定的烦恼。

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

后续步骤

- 好吧…我真的能和这根蜡烛说话吗?

- 好的…跳转到下一步

常见问题解答

5. 阅读内容

现在,您已经从 navigator.bluetooth.requestDevice 的 promise 中返回了 BluetoothDevice,接下来该怎么做?我们通过调用 device.gatt.connect() 连接到包含蓝牙服务和特征定义的蓝牙远程 GATT 服务器:

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

读取设备名称

在这里,我们连接到了 PLAYBULB 蜡烛蓝牙设备的 GATT 服务器。现在,我们想要获取主要 GATT 服务(之前通告为 0xFF02),并读取属于此服务的设备名称特征 (0xFFFF)。只需向 PlaybulbCandle 类添加一个新方法 getDeviceName,并使用 device.gatt.getPrimaryService 和 service.getCharacteristic,即可轻松实现此目的。characteristic.readValue 方法实际上会返回一个 DataView,我们只需使用 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);
      });
    }

我们通过在连接后调用 playbulbCandle.getDeviceName,将此内容添加到 app.js 中,并显示设备名称。

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

此时,请在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或直接刷新现有页面。确保 PLAYBULB Candle 已开启,然后点击页面上的“Connect”按钮,您应该会在颜色选择器下方看到设备名称。

Screen Shot 2016-11-16 at 3.29.21 PM.png

朗读电池电量

PLAYBULB Candle 蓝牙设备中还提供标准电池电量蓝牙特征,其中包含设备的电池电量。这意味着我们可以使用标准名称,例如 battery_service 表示蓝牙 GATT 服务 UUID,battery_level 表示蓝牙 GATT 特征 UUID。

我们向 PlaybulbCandle 类添加一个新的 getBatteryLevel 方法,并读取电池电量百分比。

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

我们还需要更新 options JavaScript 对象,以便将电池服务添加到 optionalServices 键,因为 PLAYBULB Candle 蓝牙设备不会通告该服务,但仍必须访问该服务。

playbulbCandle.js

      let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}],
                     optionalServices: ['battery_service']};
      return navigator.bluetooth.requestDevice(options)

与之前一样,我们在获取设备名称并显示电池电量后,通过调用 playbulbCandle.getBatteryLevel 将其插入 app.js。

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 + '%';
}

此时,请在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或直接刷新现有页面。点击页面上的“连接”按钮,您应该会看到设备名称和电池电量。

Screen Shot 2016-11-16 at 3.29.21 PM.png

后续步骤

- 如何更改此灯泡的颜色?这就是我来这里的原因!

- 相信我,你马上就要成功了…

常见问题解答

6. 更改颜色

只需向通告为 0xFF02 的主要 GATT 服务中的蓝牙特征 (0xFFFC) 写入一组特定命令,即可轻松更改颜色。例如,将 PLAYBULB Candle 调成红色,就是写入一个 8 位无符号整数数组,其值等于 [0x00, 255, 0, 0],其中 0x00 是白色饱和度,255, 0, 0 分别是红色、绿色和蓝色值。

我们将使用 characteristic.writeValue 在 PlaybulbCandle 类的新 setColor 公共方法中实际向蓝牙特征写入一些数据。我们还会在 promise 得到履行时返回实际的红、绿、蓝值,以便稍后在 app.js 中使用:

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

我们来更新 app.js 中的 changeColor 函数,以便在选中“无效果”单选按钮时调用 playbulbCandle.setColor。当用户点击颜色选择器画布时,全局 r, g, b 颜色变量已设置。

app.js

function changeColor() {
  var effect = document.querySelector('[name="effectSwitch"]:checked').id;
  if (effect === 'noEffect') {
    playbulbCandle.setColor(r, g, b).then(onColorChanged);
  }
}

此时,请在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或直接刷新现有页面。点击页面上的“连接”按钮,然后点击颜色选择器,即可随意更改 PLAYBULB Candle 的颜色。

Screen Shot 2016-11-16 at 3.31.37 PM.png

更多蜡烛效果

如果您之前点过蜡烛,就会知道烛光不是静止的。幸运的是,在通告为 0xFF02 的主要 GATT 服务中,还有另一个蓝牙特征 (0xFFFB),可让用户设置一些蜡烛效果。

例如,可以通过编写 [0x00, r, g, b, 0x04, 0x00, 0x01, 0x00] 来设置“蜡烛效果”。您还可以使用 [0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00] 设置“闪烁效果”。

我们将 setCandleEffectColor 和 setFlashingColor 方法添加到 PlaybulbCandle 类中。

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

我们来更新 app.js 中的 changeColor 函数,以便在选中“Candle Effect”单选按钮时调用 playbulbCandle.setCandleEffectColor,在选中“Flashing”单选按钮时调用 playbulbCandle.setFlashingColor。这次,如果您不介意,我们将使用switch。

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

此时,请在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或直接刷新现有页面。点击页面上的“连接”按钮,然后体验蜡烛和闪烁效果。

Screen Shot 2016-11-16 at 3.33.23 PM.png

后续步骤

- 就这些?3 个效果不佳的蜡烛特效?这就是我来这里的原因吗?

- 还有更多,但这次您需要自己探索。

7. 再加一把劲

现在,我们已经完成了!您可能认为应用已接近完成,但实际上还未完成。让我们看看您是否真的理解了您在本 Codelab 中复制粘贴的内容。现在,您需要自行完成以下操作,让这款应用更加出色。

添加缺失的效果

以下是缺失效果的数据:

  • Pulse:[0x00, r, g, b, 0x01, 0x00, 0x09, 0x00](您可能需要调整其中的 r, g, b 值)
  • 彩虹:[0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00](癫痫患者可能需要避开此效果)
  • 彩虹渐变:[0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x26, 0x00]

这基本上意味着向 PlaybulbCandle 类添加新的 setPulseColor、setRainbow 和 setRainbowFade 方法,并在 changeColor 中调用它们。

修正“无效果”

您可能已经注意到,“无效果”选项不会重置任何正在进行的效果,虽然这只是一个小问题,但仍然存在。接下来,让我们解决这个问题。在 setColor 方法中,您需要先通过新的类变量 _isEffectSet 检查效果是否正在运行,如果 true,则在设置新颜色之前关闭效果,并使用以下数据:[0x00, r, g, b, 0x05, 0x00, 0x01, 0x00]。

输入设备名称

这个问题很简单!写入自定义设备名称就像写入之前的蓝牙设备名称特征一样简单。我建议使用 TextEncoder encode 方法获取包含设备名称的 Uint8Array。

然后,我会向 document.querySelector('#deviceName') 添加一个“输入”eventListener,并调用 playbulbCandle.setDeviceName,以保持简单。

我个人将我的蜡烛命名为 PLAY💡 CANDLE!

8. 就是这样!

您学到的内容

  • 如何在 JavaScript 中与附近的蓝牙设备互动
  • 如何使用 ES2015 类、箭头函数、Map 和 Promise

后续步骤