1. 这款游戏的主题是什么?
在这个开明的 Codelab 中,您将学习如何借助 Web Bluetooth API 使用 JavaScript 来控制 PLAYBULB LED 无火蜡烛。在此过程中,您还可以试用 JavaScript ES2015 功能,例如类、箭头函数、映射和 Promise。
学习内容
- 如何在 JavaScript 中与附近的蓝牙设备交互
- 如何使用 ES2015 类、箭头函数、Map 和 Promise
所需条件
- 对 Web 开发有基本的了解
- 具备蓝牙低功耗 (BLE) 和通用属性配置文件 (GATT) 的基础知识
- 您选择的文本编辑器
- 一台安装了 Chrome 浏览器应用和一根 USB 微型转 USB 线的 Mac、Chromebook 或 Android M 设备。
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 服务器
虽然您可以随意使用自己的 Web 服务器,但此 Codelab 的设计宗旨是与 Chrome Web 服务器很好地搭配使用。如果您尚未安装此应用,可以从 Chrome 应用商店安装。
安装 Web Server for Chrome 应用后,点击书签栏上的“应用”快捷方式:
在随后出现的窗口中,点击 Web 服务器图标:
接下来您会看到此对话框,您可以在其中配置本地网络服务器:
点击选择文件夹按钮,然后选择已克隆(或已取消归档)代码库的根目录。这样,您就可以通过 Web 服务器对话框(在 Web Server 网址(s) 部分)中突出显示的网址来处理正在进行的工作。
在“选项”下,选中“自动显示 index.html”旁边的复选框,如下所示:
现在,在网络浏览器中访问您的网站(点击突出显示的 Web Server 网址),您将看到如下页面:
如果您想查看此应用在 Android 手机上的显示效果,请在 Android 设备上启用远程调试并设置端口转发(端口号默认为 8887)。然后,您只需在 Android 手机上打开一个新 Chrome 标签页并转到 http://localhost:8887 即可。
后续步骤
此时,此 Web 应用功能不多。让我们开始添加蓝牙支持吧!
4. 探索蜡烛
首先,我们编写一个库,该库使用适用于 PLAYBULB Candle 蓝牙设备的 JavaScript ES2015 类。
保持冷静。类语法没有在 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 设备会持续播送(如果尚未配对)常量蓝牙 GATT 服务 UUID(其简写形式为 0xFF02
),因此我们可以简单地定义一个常量,并将其添加到 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
发现附近的蓝牙设备。因此,我们会在用户点击“Connect”时调用 connect
方法。按钮(位于 app.js
文件中):
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 蓝牙设备已关闭,您可能会收到错误消息。如果出现这种情况,请开启该功能,然后再次继续操作。
强制性奖金
不知道您是谁,但我在代码中看到太多 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 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 蜡烛已开启,然后点击“连接”按钮,您应该会在颜色选择器下方看到设备名称。
读取电池电量
PLAYBULB 蜡烛蓝牙设备中还有标准电池电量蓝牙特性,其中包含设备的电池电量。这意味着我们可以使用标准名称,例如 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 蜡烛蓝牙设备不会通告该服务,但该服务仍然必须访问该服务。
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 K 线变为红色,相当于写入了一个等于 [0x00, 255, 0, 0]
的 8 位无符号整数数组,其中 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 蜡烛的颜色,次数不限。
蜡烛效果
如果您以前点过蜡烛,那么您就知道蜡烛的灯光不是静态的。幸运的是,主要 GATT 服务中的另一个蓝牙特征 (0xFFFB
) 通告为 0xFF02
,可让用户设置一些蜡烛效果。
设置“蜡烛效果”例如,可以通过写入 [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
函数,以便在“烛光效果”发生时调用 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;
}
}
此时,您可以在网络浏览器中访问您的网站(点击网络服务器应用中突出显示的网络服务器网址),或直接刷新现有页面。点击“连接”按钮,并使用“蜡烛”和“闪烁效果”进行演奏。
后续步骤
- 就这些?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
。
然后,添加一个“input”为简单起见,将 eventListener
设置为 document.querySelector('#deviceName')
并调用 playbulbCandle.setDeviceName
。
我个人取名给我的“PLAY💡?”CANDLE!
8. 大功告成!
您学到的内容
- 如何在 JavaScript 中与附近的蓝牙设备交互
- 如何使用 ES2015 类、箭头函数、Map 和 Promise
后续步骤
- 详细了解 Web Bluetooth API
- 浏览官方的 Web 蓝牙示例和演示
- 查看飞翔不爽的猫