1. 内容简介

在此启发性 Codelab 中,您将学习如何仅使用 JavaScript 通过 Web 蓝牙 API 控制 PLAYBULB LED 无焰蜡烛。在此过程中,您还将体验 JavaScript ES2015 功能,例如类、箭头函数、Map 和Promise。
学习内容
- 如何在 JavaScript 中与附近的蓝牙设备互动
- 如何使用 ES2015 类、箭头函数、Map 和 Promise
所需条件
- 对 Web 开发有基本的了解
- 具备 低功耗蓝牙 (BLE) 和 Generic Attribute Profile (GATT) 的基本知识
- 您选择的文本编辑器
- Mac、Chromebook 或 Android M 设备,并配备 Chrome 浏览器应用和 USB Micro 转 USB 线缆。
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。
安装并验证网络服务器
尽管您可以随意使用自己的网络服务器,但此 Codelab 旨在与 Chrome Web 服务器配合使用。如果您尚未安装该应用,则可以通过 Chrome 应用商店安装。
安装 Web Server for Chrome 应用后,点击书签栏中的“应用”快捷方式:

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

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

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

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

如果您想在 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);
});
});
运行应用
此时,在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或只需刷新现有网页即可。点击绿色的“连接”按钮,在选择器中选择设备,然后使用 Ctrl + Shift + J 键盘快捷键打开您最喜欢的开发者工具控制台,并注意记录的 BluetoothDevice 对象。

如果蓝牙处于关闭状态和/或 PLAYBULB Candle 蓝牙设备处于关闭状态,您可能会收到错误消息。在这种情况下,请开启蓝牙和/或 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 的承诺中获得 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 Candle 蓝牙设备的 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 已开启,然后点击页面上的“连接”按钮,您应该会在颜色选择器下方看到设备名称。

读取电池电量
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 + '%';
}
此时,在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或只需刷新现有网页即可。点击页面上的“连接”按钮,您应该会看到设备名称和电池电量。
后续步骤
- 如何更改此灯泡的颜色?这就是我来这里的目的!
- 相信我,您即将大功告成...
常见问题解答
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 的颜色。
更多蜡烛效果
如果您之前点过蜡烛,就会知道烛光不是静止的。幸运的是,在宣传为 0xFF02 的主要 GATT 服务中,还有另一个蓝牙特征 (0xFFFB) 可让用户设置一些蜡烛效果。
例如,如需设置“蜡烛效果”,可写入 [0x00, r, g, b, 0x04, 0x00, 0x01, 0x00]。您还可以使用 [0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00] 设置“闪烁效果”。
我们向 PlaybulbCandle 类添加 setCandleEffectColor 和 setFlashingColor 方法。
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 函数,以便在选中“烛光效果”单选按钮时调用 playbulbCandle.setCandleEffectColor,并在选中“闪烁”单选按钮时调用 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;
}
}
此时,在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或只需刷新现有网页即可。点击页面上的“连接”按钮,体验蜡烛和闪烁效果。
后续步骤
- 就这些吗?3 种不良蜡烛效果?难道这就是我来这里的目的?
- 还有更多,但这次您需要自行解决。
7. 再加一把劲
我们终于成功了!您可能认为游戏即将结束,但应用尚未结束。让我们看看您是否真正理解了在本 Codelab 中复制粘贴的内容。现在,您需要自行完成以下操作,才能让这款应用大放异彩。
添加缺失的效果
以下是缺失效果的数据:
- 脉冲:
[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
后续步骤
- 详细了解 Web Bluetooth API
- 浏览官方 Web 蓝牙示例和演示
- 看看飞翔的“不爽猫”

