通过 JavaScript 与蓝牙设备通信
Web Bluetooth API 允许网站与蓝牙设备进行通信。
如果我告诉您网站能以安全和隐私保护的方式与附近的蓝牙设备进行通信,您会怎么想?如此一来,心率监测器、会唱歌的灯,甚至海龟都可以直接与网站交互了。
到目前为止,仅有部分针对特定平台的应用可以实现与蓝牙设备的交互。Web Bluetooth API 旨在改变这一现状,以期将此功能赋予 Web 浏览器。
前言 #
本文假定您对低功耗蓝牙 (BLE) 和通用属性配置文件(GATT) 的工作原理已有一定的了解。
尽管 Web Bluetooth API 规范尚未最终确定,但规范的作者们正在积极寻找热情的开发人员来试用此 API,并就规范和实施提供反馈。
ChromeOS、Chrome for Android 6.0、Mac (Chrome 56) 和 Windows 10 (Chrome 70) 中有一个可用的 Web Bluetooth API 子集。这意味着您应该能够请求并连接到附近的低功耗蓝牙设备、读取/写入蓝牙特性、接收 GATT 通知、了解蓝牙设备何时断开连接,甚至读取和写入蓝牙描述符。有关更多信息,请参阅 MDN 的浏览器兼容性表。
对于 Linux 和更早版本的 Windows,请在 about://flags
中启用 #experimental-web-platform-features
标志。
初步试用 #
为了尽可能多地获得开发人员对使用 Web Bluetooth API 的一手反馈,Chrome 此前已在 Chrome 53 中添加了此功能,以供在 ChromeOS、Android 和 Mac 中初步试用。
试用已于 2017 年 1 月顺利结束。
安全要求 #
要了解安全权衡,推荐您查阅由 Chrome 团队的软件工程师 Jeffrey Yasskin 发表的 Web 蓝牙安全模型帖子,他致力于 Web Bluetooth API 规范。
仅限 HTTPS #
由于该试验性 API 是添加到 Web 中的一项强大的新功能,因此仅可用于保护上下文。这意味着您在构建时需要考虑 TLS 。
需要用户手势 #
作为一项安全功能,使用 navigator.bluetooth.requestDevice
发现蓝牙设备必须由用户手势(例如触摸或鼠标点击)触发。我们正在谈论对 pointerup
、 click
和 touchend
事件的侦听。
button.addEventListener('pointerup', function(event) {
// Call navigator.bluetooth.requestDevice
});
了解代码 #
Web Bluetooth API 严重依赖于 JavaScript Promises 。如果您不熟悉它们,请参阅这个很棒的 Promises 教程。还有一件事, () => {}
是简单的 ECMAScript 2015 箭头函数。
请求蓝牙设备 #
此版本的 Web Bluetooth API 规范允许以 Central 角色运行的网站通过 BLE 连接实现到远程 GATT 服务器的连接。支持使用蓝牙 4.0 或更高版本的设备之间的通信。
当网站使用 navigator.bluetooth.requestDevice
请求访问附近的设备时,浏览器会通过设备选择器提示用户,用户可以在该选择器中选择一台设备或简单地取消请求。
navigator.bluetooth.requestDevice()
函数采用定义筛选器的强制对象。这些筛选器仅用于返回与某些公布的蓝牙 GATT 服务和/或设备名称匹配的设备。
服务筛选器 #
例如,要请求公布蓝牙 GATT 电池服务的蓝牙设备:
navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => { /* … */ })
.catch(error => { console.error(error); });
如果您的蓝牙 GATT 服务不在标准化蓝牙 GATT 服务列表中,您可以提供完整的蓝牙 UUID 或简短的 16 位或 32 位形式。
navigator.bluetooth.requestDevice({
filters: [{
services: [0x1234, 0x12345678, '99999999-0000-1000-8000-00805f9b34fb']
}]
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });
名称筛选器 #
您还可以根据使用 name
筛选键公布的设备名称请求蓝牙设备,甚至可以使用 namePrefix
筛选键根据此名称的前缀来请求蓝牙设备。请注意,在这种情况下,您还需要定义 optionalServices
键才能访问未包含在服务筛选器中的任何服务。否则,稍后在尝试访问它们时会出现错误。
navigator.bluetooth.requestDevice({
filters: [{
name: 'Francois robot'
}],
optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });
制造商数据筛选器 #
还可以根据使用 manufacturerData
筛选键公布的制造商规范数据请求蓝牙设备。此键是一个对象数组,其中包含一个名为 companyIdentifier
的强制蓝牙公司标识符键。您还可以提供一个数据前缀,从以它开头的蓝牙设备中筛选制造商数据。请注意,您还需要定义 optionalServices
键才能访问未包含在服务筛选器中的任何服务。否则,稍后在尝试访问它们时会出现错误。
// Filter Bluetooth devices from Google company with manufacturer data bytes
// that start with [0x01, 0x02].
navigator.bluetooth.requestDevice({
filters: [{
manufacturerData: [{
companyIdentifier: 0x00e0,
dataPrefix: new Uint8Array([0x01, 0x02])
}]
}],
optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });
掩码还可以与数据前缀一起使用,以匹配制造商数据中的某些模式。请查看蓝牙数据筛选器解释器以了解更多信息。
没有筛选器 #
最后,您可以使用 acceptAllDevices
键代替 filters
来显示附近的所有蓝牙设备。您还需要定义 optionalServices
键才能访问某些服务。否则,稍后在尝试访问它们时会出现错误。
navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });
连接到蓝牙设备 #
那么现在有一个 BluetoothDevice
该怎么办?让我们将其连接到包含服务和特征定义的蓝牙远程 GATT 服务器。
navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
// Human-readable name of the device.
console.log(device.name);
// Attempts to connect to remote GATT Server.
return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });
读取蓝牙特征 #
现在,我们被连接到了远程蓝牙设备的 GATT 服务器。我们想要获取一个主 GATT 服务并读取属于该服务的特征。例如,让我们尝试读取设备电池的当前电量。
在下面的示例中,battery_level
是标准的电池电量特征 。
navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => device.gatt.connect())
.then(server => {
// Getting Battery Service…
return server.getPrimaryService('battery_service');
})
.then(service => {
// Getting Battery Level Characteristic…
return service.getCharacteristic('battery_level');
})
.then(characteristic => {
// Reading Battery Level…
return characteristic.readValue();
})
.then(value => {
console.log(`Battery percentage is ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });
如果您在使用自定义蓝牙 GATT 特征,则可以向 service.getCharacteristic
提供完整的蓝牙 UUID 或简短的 16 位或 32 位形式。
请注意,您还可以在特征上添加 characteristicvaluechanged
事件侦听器来处理读取其值。请查看读取特征值更改示例,了解如何选择性地处理即将到来的 GATT 通知。
…
.then(characteristic => {
// Set up event listener for when characteristic value changes.
characteristic.addEventListener('characteristicvaluechanged',
handleBatteryLevelChanged);
// Reading Battery Level…
return characteristic.readValue();
})
.catch(error => { console.error(error); });
function handleBatteryLevelChanged(event) {
const batteryLevel = event.target.value.getUint8(0);
console.log('Battery percentage is ' + batteryLevel);
}
写入蓝牙特征 #
写入蓝牙 GATT 特征就像读取它一样简单。这一次,让我们使用心率控制点将心率监测设备上的能量消耗字段的值重置为 0。
我保证这里没有魔法。这一切都在心率控制点特征页面中进行了解释。
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_control_point'))
.then(characteristic => {
// Writing 1 is the signal to reset energy expended.
const resetEnergyExpended = Uint8Array.of(1);
return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
console.log('Energy expended has been reset.');
})
.catch(error => { console.error(error); });
接收 GATT 通知 #
现在,让我们看看当设备上的心率测量特征发生变化时如何得到通知:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => characteristic.startNotifications())
.then(characteristic => {
characteristic.addEventListener('characteristicvaluechanged',
handleCharacteristicValueChanged);
console.log('Notifications have been started.');
})
.catch(error => { console.error(error); });
function handleCharacteristicValueChanged(event) {
const value = event.target.value;
console.log('Received ' + value);
// TODO: Parse Heart Rate Measurement value.
// See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}
通知示例向您展示了如何使用 stopNotifications()
停止通知并正确删除添加的 characteristicvaluechanged
事件侦听器。
与蓝牙设备断开连接 #
为了提供更好的用户体验,您可能需要侦听断开连接事件并邀请用户重新连接:
navigator.bluetooth.requestDevice({ filters: [{ name: 'Francois robot' }] })
.then(device => {
// Set up event listener for when device gets disconnected.
device.addEventListener('gattserverdisconnected', onDisconnected);
// Attempts to connect to remote GATT Server.
return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });
function onDisconnected(event) {
const device = event.target;
console.log(`Device ${device.name} is disconnected.`);
}
您还可以调用 device.gatt.disconnect()
以断开您的网络应用与蓝牙设备的连接。这将触发现有的 gattserverdisconnected
事件侦听器。请注意,如果另一个应用已经与蓝牙设备通信,则不会停止蓝牙设备通信。请查看设备断开连接示例和自动重新连接示例以深入了解。
读写蓝牙描述符 #
蓝牙 GATT 描述符是描述特征值的属性。您可以以类似于蓝牙 GATT 特征的方式读取和写入它们。
例如,让我们看看如何读取设备健康温度计测量间隔的用户描述。
在下面的示例中,health_thermometer
是健康温度计服务, measurement_interval
是测量间隔特征, gatt.characteristic_user_description
是特征性用户描述描述符。
navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => descriptor.readValue())
.then(value => {
const decoder = new TextDecoder('utf-8');
console.log(`User Description: ${decoder.decode(value)}`);
})
.catch(error => { console.error(error); });
现在我们已经读取了设备健康温度计测量间隔的用户描述,让我们看看如何更新它并写入自定义值。
navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => {
const encoder = new TextEncoder('utf-8');
const userDescription = encoder.encode('Defines the time between measurements.');
return descriptor.writeValue(userDescription);
})
.catch(error => { console.error(error); });
示例、演示和代码实验室 #
以下所有的 Web 蓝牙示例均已成功测试。为了充分享受这些示例,建议您安装 BLE Peripheral Simulator Android 应用,该应用可以模拟提供电池服务、心率服务或健康温度计服务的 BLE 外围设备。
初学者 #
- 设备信息- 从 BLE 设备检索基本的设备信息。
- 电池电量- 从公布电池信息的 BLE 设备中检索电池信息。
- 重置能量- 重置公布心率的 BLE 设备消耗的能量。
- 特征属性- 显示 BLE 设备的特定特征的所有属性。
- 通知- 启动和停止 BLE 设备的特征通知。
- 设备断开连接- 断开连接,并在连接到 BLE 设备后收到断开连接的通知。
- 获取特征- 从 BLE 设备获取已公布服务的所有特性。
- 获取描述符- 从 BLE 设备获取已公布服务的所有特征的描述符。
- 制造商数据筛选器- 从匹配制造商数据的 BLE 设备中检索基本的设备信息。
组合多个操作 #
- GAP 特征- 获取 BLE 设备的所有 GAP 特征。
- 设备信息特征- 获取 BLE 设备的所有设备信息特征。
- 链路丢失- 设置 BLE 设备的警报级别特征(readValue 和 writeValue)。
- 发现服务和特征- 从 BLE 设备发现所有可访问的主要服务及其特征。
- 自动重新连接- 使用指数退避算法重新连接到断开连接的 BLE 设备。
- 读取已更改的特征值- 读取电池电量和来自 BLE 设备的更改通知。
- 读取描述符- 从 BLE 设备读取服务的所有特征描述符。
- 写入描述符- 写入 BLE 设备上的描述符“特征用户描述”。
另请查阅我们的精心策划的网络蓝牙演示和官方网络蓝牙代码实验室。
库 #
- web-bluetooth-utils 是一个 npm 模块,向 API 添加一些便捷函数。
- 最流行的 Node.js BLE 中央模块 noble 中提供了 Web Bluetooth API 填充码。这使您无需 WebSocket 服务器或其他插件即可 webpack(模块化管理和打包)/browserify(在浏览器端组织) noble。
- angular-web-bluetooth 是 Angular的一个模块,它提取配置 Web Bluetooth API 所需的所有样板。
工具 #
- Get Started with Web Bluetooth 是一款简单的网络应用,其会生成与蓝牙设备进行交互的所有 JavaScript 样板代码。您只需输入设备名称、服务、特征,定义其属性即可。
- 如果您已经是蓝牙开发人员,Web Bluetooth Developer Studio 插件还将为您的蓝牙设备生成 Web Bluetooth JavaScript 代码。
提示 #
在 Chrome 中的 about://bluetooth-internals
位置可查看蓝牙内部页面,以便您可以检查附近蓝牙设备的所有信息:状态、服务、特征和描述符。

还建议您查阅官方如何提交 Web 蓝牙错误页面,因为调试蓝牙有时比较麻烦。
下一步行动 #
首先检查浏览器和平台实现状态,以了解当前正在实现 Web Bluetooth API 的哪些部分。
虽然它仍然不完整,但我们可以了解不久的将来它要实现的功能:
- 将使用
navigator.bluetooth.requestLEScan()
扫描附近的 BLE 广告。 - 新的
serviceadded
事件将跟踪新发现的蓝牙 GATT 服务,而serviceremoved
事件将跟踪已删除的服务。向蓝牙 GATT 服务中添加任何特征和/或描述符或从中删除这些时,将触发新的servicechanged
事件。
展示您对 API 的支持 #
您是否打算使用 Web Bluetooth API?您公开的支持将帮助 Chrome 团队竭力打造功能,并为其他浏览器供应商提供动力,这弥足珍贵。
请向 @ChromiumDev 发送带有 #WebBluetooth
标签的推文,让我们知道您在哪里以及以何种方式在使用它。
资源 #
- 堆栈溢出: https://stackoverflow.com/questions/tagged/web-bluetooth
- Chrome 功能状态: https://www.chromestatus.com/feature/5264933985976320
- Chrome 实现错误: https://crbug.com/?q=component:Blink>Bluetooth
- Web 蓝牙规范: https://webbluetoothcg.github.io/web-bluetooth
- 规范问题: https://github.com/WebBluetoothCG/web-bluetooth/issues
- BLE 外设模拟器应用: https://github.com/WebBluetoothCG/ble-test-peripheral-android
致谢 #
非常感谢 Kayce Basques 审阅本文。首图提供者:美国博尔德的 SparkFun Electronics。