借助 Web Bluetooth API,网站可以与蓝牙设备通信。
如果我告诉您,网站可以以安全且可保护隐私的方式与附近的蓝牙设备通信,您会怎么想?这样一来,心率监测器、会唱歌的灯泡,甚至乌龟都可以直接与网站互动。
到目前为止,只有平台专用应用才能与蓝牙设备互动。Web Bluetooth API 旨在改变这一现状,并将其引入到网络浏览器中。
开始前须知
本文档假定您对蓝牙低功耗 (BLE) 和通用属性配置文件的运作方式有一些基本了解。
虽然 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 Bluetooth 安全模型一文,他负责 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 promise。如果您不熟悉 Promise,请查看这篇出色的 Promise 教程。还有一点,() => {}
是 ECMAScript 2015 箭头函数。
请求蓝牙设备
此版本的 Web Bluetooth API 规范允许在“中央”角色中运行的网站通过 BLE 连接连接到远程 GATT 服务器。它支持实现蓝牙 4.0 或更高版本的设备之间的通信。
当网站使用 navigator.bluetooth.requestDevice
请求访问附近的设备时,浏览器会向用户显示设备选择器,用户可以在其中选择一台设备或取消请求。
navigator.bluetooth.requestDevice()
函数接受一个用于定义过滤条件的必需对象。这些过滤器用于仅返回与某些已通告的蓝牙 GATT 服务和/或设备名称匹配的设备。
“服务”过滤条件
例如,如需请求蓝牙设备通告 Bluetooth 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); });
您还可以将掩码与数据前缀搭配使用,以匹配制造商数据中的某些模式。如需了解详情,请参阅 Bluetooth 数据过滤器说明。
排除过滤条件
借助 navigator.bluetooth.requestDevice()
中的 exclusionFilters
选项,您可以从浏览器选择器中排除某些设备。它可用于排除与更广泛的过滤条件匹配但不受支持的设备。
// Request access to a bluetooth device whose name starts with "Created by".
// The device named "Created by Francois" has been reported as unsupported.
navigator.bluetooth.requestDevice({
filters: [{
namePrefix: "Created by"
}],
exclusionFilters: [{
name: "Created by Francois"
}],
optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });
0 个过滤条件
最后,您可以使用 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()
来断开 Web 应用与蓝牙设备之间的连接。这将触发现有的 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); });
示例、演示和 Codelab
以下所有 Web Bluetooth 示例均已成功测试。为了充分利用这些示例,建议您安装 [BLE 外围设备模拟器 Android 应用],该应用可模拟具有电池服务、心率服务或健康温度计服务的 BLE 外围设备。
新手
- 设备信息 - 从 BLE 设备检索基本设备信息。
- 电池电量 - 从通告电池信息的 BLE 设备检索电池信息。
- 重置能耗 - 重置 BLE 设备在广告心率时消耗的能量。
- 特征属性 - 显示 BLE 设备中特定特征的所有属性。
- 通知 - 启动和停止 BLE 设备的特征通知。
- 设备断开连接 - 断开与 BLE 设备的连接,并在连接后收到断开连接的通知。
- 获取特性 - 从 BLE 设备获取所宣传服务的所有特性。
- 获取描述符 - 从 BLE 设备获取已通告服务的所有特征的描述符。
- 制造商数据过滤器 - 从与制造商数据匹配的 BLE 设备检索基本设备信息。
- 排除项过滤条件 - 从具有基本排除项过滤条件的 BLE 设备检索基本设备信息。
组合多项操作
- GAP 特性 - 获取 BLE 设备的所有 GAP 特性。
- 设备信息特征 - 获取 BLE 设备的所有设备信息特征。
- 链接丢失 - 设置 BLE 设备的 Alert Level 特征(readValue 和 writeValue)。
- 发现服务和特性 - 从 BLE 设备发现所有可访问的主要服务及其特性。
- 自动重新连接 - 使用指数后退算法重新连接到已断开连接的 BLE 设备。
- 读取特征值已更改 - 读取电池电量并在 BLE 设备发生变化时收到通知。
- 读取描述符 - 从 BLE 设备读取服务的所有特征描述符。
- 写入描述符 - 向 BLE 设备上的“Characteristic User Description”描述符写入数据。
您还可以查看我们的精选 Web Bluetooth 演示和官方 Web Bluetooth Codelab。
库
- web-bluetooth-utils 是一个 npm 模块,用于向 API 添加一些便捷函数。
- 最常用的 Node.js BLE 中央模块 noble 中提供了 Web Bluetooth API 代理。这样一来,您无需 WebSocket 服务器或其他插件即可 webpack/browserify noble。
- angular-web-bluetooth 是 Angular 的模块,用于提取配置 Web Bluetooth API 所需的所有样板代码。
工具
- Web Bluetooth 入门是一个简单的 Web 应用,它会生成所有 JavaScript 样板代码,以便开始与蓝牙设备交互。输入设备名称、服务、特征,定义其属性,即可开始使用。
- 如果您已经是蓝牙开发者,Web Bluetooth Developer Studio 插件还会为您的蓝牙设备生成 Web Bluetooth JavaScript 代码。
提示
Chrome 中提供了 Bluetooth Internals 页面(网址为 about://bluetooth-internals
),以便您检查附近蓝牙设备的所有信息:状态、服务、特征和描述符。
我还建议您查看官方的如何提交 Web Bluetooth bug 页面,因为调试蓝牙有时可能很难。
后续步骤
请先查看浏览器和平台实现状态,了解 Web Bluetooth API 的哪些部分目前正在实现。
虽然该计划仍在完善中,但我们还是抢先为您介绍了近期即将推出的功能:
navigator.bluetooth.requestLEScan()
会扫描附近的 BLE 广告。- 新的
serviceadded
事件将跟踪新发现的蓝牙 GATT 服务,而serviceremoved
事件将跟踪已移除的服务。当有任何特征和/或描述符被添加或移除到蓝牙 GATT 服务时,系统会触发新的servicechanged
事件。
显示对该 API 的支持
您打算使用 Web Bluetooth API 吗?您的公开支持有助于 Chrome 团队确定功能的优先级,并向其他浏览器供应商表明支持这些功能的重要性。
使用 #WebBluetooth
标签向 @ChromiumDev 发送推文,告诉我们您在哪里以及如何使用该工具。
资源
致谢
感谢 Kayce Basques 审核本文。 主打图片由美国科罗拉多州博尔德的 SparkFun Electronics 提供。