几周前,我偶然发现了一个我以前不知道存在的浏览器 API;那就是 Web 蓝牙 API。看起来它已经在开发中有 7 年以上的时间了,我很高兴大多数浏览器现在都支持它。我非常喜欢“一次构建,到处运行”的理念,这也是 Web 蓝牙令人兴奋的原因之一。您无需为 Android、iOS、macOS 或 Windows 编写原生应用程序来与蓝牙外设进行交互。您所需做的只是构建 Web 应用程序,它就可以在各个平台上运行,为您节省大量的开发时间。
在本文中,我将向您展示如何使用 Web 蓝牙 API 构建与蓝牙外设进行交互的应用程序。我们将一起构建一个由 Web 蓝牙驱动的应用程序,用于显示来自蓝牙智能手表或蓝牙健身追踪器等启用了蓝牙的心率监测设备的统计信息。如果您拥有其中任何一种设备,请将其准备好!
💡 完整应用程序的实时演示可以在这里找到,以及它的源代码可以在此 GitHub 仓库中找到。
Web 蓝牙是如何工作的?
就像您已经猜到的那样,蓝牙不是一种 web 技术。它不遵循标准的 OSI 模型,也不使用 TCP/IP 协议。蓝牙有自己独立的模型,以及其独特的协议栈。一个重要的问题是,如何可能从 web 浏览器与蓝牙外设进行交互?这一切是如何运作的呢?
多亏了 GATT(通用 ATTribute)标准,蓝牙外设能够使用一种通用的数据协议与其他设备进行通信。这一标准得到了现代 web 浏览器的支持,因此 Web 蓝牙 API 应运而生。GATT 使得可以将蓝牙外设描述为服务器,其具有若干服务,这些服务由它们的特征控制。这与标准的 web 模型非常契合,即客户端(web 浏览器)连接到服务器(蓝牙外设),并可以通过对相应特征发出读/写请求来利用服务器上的服务。
为了让您更好地理解,想象一下一个带有集成扬声器的蓝牙智能灯泡。外设本身被视为一个服务器。在这个例子中,该服务器有两个服务;一个扬声器服务和一个灯泡服务。灯泡服务具有一个开关特征,使得可以打开和关闭灯泡。因此,可以通过将适当的数据写入开关特征来控制灯泡。
这是关于 Web 蓝牙工作原理的快速概述。现在,让我们将这个概念付诸实践,通过构建一个简单的心率监测应用程序来演示。
构建蓝牙心率监测应用程序
为了演示到目前为止讨论的概念,并向您展示使用 Web 蓝牙进行构建的简便性,让我们一起构建一个有趣的应用程序。您需要对 JavaScript 有基本的了解,还需要一个带有心率传感器的蓝牙外设。大多数智能手表或健身追踪器都具有这些功能,但如果您手头没有这些设备,您可以使用智能手机设置一个模拟器。
💡 请参阅此指南使用智能手机设置蓝牙模拟器。
为了保持简单,我在这个 zip 文件夹中提供了一个起始代码,其中包含应用程序的基本 UI。index.js
文件大部分是空的,因为我们将一起构建它。要提供这些文件,您可以使用诸如 Live Server(如果您使用 VScode)之类的静态 Web 服务器,或者使用 Python 的内置 HTTP 服务器。要使用后者,在项目目录中运行以下命令:
python3 -m http.server 3000
然后打开一个浏览器。然而,在撰写本文时,Firefox 尚不支持 Web 蓝牙。
现在,我们已经有了基础,但在继续之前,打开 index.html
文件并向具有类别 error
和类别 ui
的 div
添加一个 hide
类别。处理完这些后,让我们开始编码吧!
连接到蓝牙外设
使用 Web 蓝牙,连接到外设是一个两步过程。我们需要请求设备,然后连接到它。在请求设备时,我们需要通过配置对象描述我们的应用程序支持的设备。由于我们的应用程序需要与心率监测设备进行交互,我们需要请求具有 heart_rate
服务的蓝牙外设:
let device; // 缓存连接的设备的全局变量
let heartRateChar; // 缓存心率特征的全局变量
async function requestDevice() {
const config = {
acceptAllDevices: true,
optionalServices: ["heart_rate"],
};
device = await navigator.bluetooth.requestDevice(config);
device.addEventListener("gattserverdisconnected", connectDevice); //自动重新连接
}
然后,我们使用下面的逻辑连接到设备。连接到设备后,我们需要访问其 heart_rate
服务,然后访问 heart_rate_measurement
特征。可以通过 订阅 该特征来读取心率数据。为了捕获这些数据,我们为 characteristicvaluechanged
添加了一个事件侦听器。稍后,我们将回过头来编写一个函数来解析该值并更新 UI。
async function connectDevice() {
if (device.gatt.connected) return; //如果连接已存在,则退出
const server = await device.gatt.connect();
const service = await server.getPrimaryService("heart_rate");
heartRateChar = await service.getCharacteristic("heart_rate_measurement"); heartRateChar.addEventListener("characteristicvaluechanged", (event) => {
//处理心率变化,即在 UI 上显示更新
//暂时只进行控制台记录...
console.log(event.target.value);
});
}
要接收 characteristicvaluechanged
的更新,我们需要 订阅 它的 通知。下面的开始监控函数正是这样做的。此外,它会播放心跳音效并在 UI 上启动心跳动画:
async function startMonitoring() {
await heartRateChar.startNotifications();
beatAudio.play();
heartUI.classList.remove("pause-animation");
}
现在,我们创建一个 init
函数,在 UI 上点击连接按钮时触发流程。以下是它的代码:
async function init() {
//检查浏览器是否支持 web 蓝牙
if (!navigator.bluetooth) return errorTxt.classList.remove("hide");
await requestDevice();
connectBTN.textContent = "connecting...";
await connectDevice();
connectUI.classList.add("hide"); appUI.classList.remove("hide");
await startMonitoring();
}
connectBTN.addEventListener("click", init);
保存文件,返回浏览器并重新加载。单击连接按钮应该会打开一个模态框以选择附近的蓝牙外设。连接到适当的设备后,您应该在控制台中看到来自其心率监测传感器的数据日志。太棒了!
解析和显示心率数据
您可能已经注意到,从传感器返回的数据并不像您预期的那样是十进制的。这对于蓝牙外设来说是正常的,因为它们以二进制形式通信。控制台上的输出是一个称为 DataView
的接口,它包装在蓝牙设备发送的实际值周围的 ArrayBuffer
上。这些术语可能对您来说很新,但它们并不是很复杂。让我们更仔细地看一下。
ArrayBuffer 只是字节的数组(即 8 位组成一个字节)。例如,[00000001, 00000000, 01100010]
是一个具有三个元素的 ArrayBuffer。索引 0 处的第一个元素是十进制 1,然后索引 1 处是十进制 0,索引 2 处是十进制 98。但是 ArrayBuffer 不能直接在 JavaScript 中使用,这就是 DataView 的用武之地。DataViews 包装在 ArrayBuffer 周围,以便可以对其进行读取或写入。这就是整个故事的梗概,简单吧?
由于从蓝牙外设返回的数据已经是一个 DataView,因此解析它会变得稍微容易一些。也就是说,我们需要检查设备是否通过检查 标志字节 来对其数据进行 16 位或 8 位编码。标志字节是 ArrayBuffer 中的第一项(即索引 0)。标志字节中的最后一位(LSB)指示数据是 16 位还是 8 位,即:
ArrayBuffer1 = [00000001, 00000000, 01100010]
| | | |
| | | |
{---▼--} | |
标志字节。LSB = 1;数据为 16 位
| |
| |
{--------▼-------}
转换下一个 16 位为十进制
ArrayBuffer2 = [00000000, 01100010]
| | | |
| | | |
{---▼--} | |
标志字节。LSB = 0;数据为 8 位
| |
| |
{---▼--}
转换下一个 8 位为十进制
让我们编写一个函数来解析心率,同时检查数据是否以 16 位或 8 位编码。然后,我们可以通过一个新的 handleRateChange
函数将结果显示在 UI 上,而不是我们之前编写的控制台日志。下面是代码示例:
function parseHeartRate(value) {
const is16Bits = value.getUint8(0) & 0x1;
//检查标志字节中的最后一位是否为 1
if (is16Bits) return value.getUint16(1, true);
return value.getUint8(1);
}
function handleRateChange(event) {
bpmTxt.textContent = parseHeartRate(event.target.value); //在 UI 中显示
}
async function connectDevice() {
//...先前的代码在这里
//使用 handleRateChange 作为处理函数
heartRateChar.addEventListener(
"characteristicvaluechanged",
handleRateChange
);
}
返回浏览器以测试更改。它可以工作!
停止监控心率
让我们为应用程序添加一个功能。如果用户可以开始/停止监控会很好。这可以通过 取消订阅 心率通知、暂停音频效果和动画来实现:
async function stopMonitoring() {
await heartRateChar.stopNotifications();
beatAudio.pause();
heartUI.classList.add("pause-animation");
}
现在,当点击 stop
按钮时,我们可以运行此函数。我们还将 startMonitoring
函数链接到 start
按钮:
stopBTN.addEventListener("click", stopMonitoring);
startBTN.addEventListener("click", startMonitoring);
保存并重新加载。这样,我们就完成了。你做到了!
结论
Web 蓝牙为 web 带来了许多新功能,我很期待看到开发人员和硬件制造商将其推向何种高度。我很乐意看到您使用 Web 蓝牙构建的下一个酷项目,请分享给我。