网络蓝牙简介

已发表: 2022-03-10
快速总结↬借助 Progressive Web Apps,您现在可以使用 Web 构建成熟的应用程序。 由于大量的新规范和功能,我们可以在 Web 上完成您过去需要为其编写本机应用程序的事情。 然而,到目前为止,与硬件设备对话仍然是一座遥不可及的桥梁。 多亏了 WebBluetooth,我们现在可以构建可以控制灯光、驾驶汽车甚至控制无人机的 PWA。

借助 Progressive Web Apps,Web 越来越接近原生应用程序。 但是,具有网络固有的额外好处,例如隐私和跨平台兼容性。

传统上,Web 非常擅长与网络上的服务器通信,特别是与 Internet 上的服务器通信。 现在 Web 正在向应用程序发展,我们还需要原生应用程序具有的相同功能。

过去几年在浏览器中实现的新规范和功能的数量是惊人的。 我们已经制定了处理 3D 的规范,例如 WebGL 和即将推出的 WebGPU。 我们可以流式传输和生成音频、观看视频并将网络摄像头用作输入设备。 我们还可以使用 WebAssembly 以几乎原生的速度运行代码。 此外,尽管最初是一种仅限网络的媒体,但网络已经转向由服务人员提供的离线支持。

这很好,但有一个领域几乎是原生应用程序的专属领域:与设备通信。 这是我们长期以来一直试图解决的问题,而且每个人都可能在某一时刻遇到过。 网络非常适合与服务器交谈,但不适用于与设备交谈。 例如,考虑尝试在您的网络中设置路由器。 您可能必须输入 IP 地址并通过纯 HTTP 连接使用 Web 界面,而没有任何安全性。 那只是糟糕的体验和糟糕的安全性。 最重要的是,您如何知道正确的 IP 地址是什么?

跳跃后更多! 继续往下看↓

当我们尝试创建一个尝试与设备对话的渐进式 Web 应用程序时,HTTP 也是我们遇到的第一个问题。 PWA 仅支持 HTTPS,本地设备始终只是 HTTP。 您需要一个 HTTPS 证书,为了获得证书,您需要一个带有域名的公开可用的服务器(我说的是我们本地网络上遥不可及的设备)。

因此,对于许多设备,您需要原生应用程序来设置设备并使用它们,因为原生应用程序不受 Web 平台的限制,可以为用户提供愉快的体验。 但是,我不想下载一个 500 MB 的应用程序来做到这一点。 也许您拥有的设备已经使用了几年,并且该应用程序从未更新为在您的新手机上运行。 也许您想使用台式机或笔记本电脑,而制造商只构建了一个移动应用程序。 也不是理想的体验。

WebBluetooth 是一种已在 Chrome 和三星 Internet 中实施的新规范,它允许我们从浏览器直接与蓝牙低功耗设备进行通信。 渐进式 Web 应用程序与 WebBluetooth 相结合,提供 Web 应用程序的安全性和便利性,并具有直接与设备对话的能力。

由于范围有限、音频质量差和配对问题,蓝牙的名声很差。 但是,几乎所有这些问题都已成为过去。 低功耗蓝牙是一种现代规范,除了使用相同的频谱之外,与旧蓝牙规范几乎没有关系。 每天有超过 1000 万台设备支持蓝牙。 这包括电脑和手机,还包括心率和血糖监测仪等各种设备、灯泡等物联网设备以及遥控汽车和无人机等玩具。

推荐阅读了解基于 API 的平台:产品经理指南

无聊的理论部分

由于蓝牙本身不是一种网络技术,它使用了一些我们可能不熟悉的词汇。 因此,让我们回顾一下蓝牙的工作原理和一些术语。

每个蓝牙设备要么是“中央设备”,要么是“外围设备”。 只有中央设备可以发起通信,并且只能与外围设备通信。 中央设备的一个示例是计算机或移动电话。

外围设备无法启动通信,只能与中央设备通信。 此外,外围设备只能同时与一个中央设备通信。 外围设备不能与另一个外围设备通信。

中间的电话,与多个外围设备通话,例如无人机、机器人玩具、心率监测器和灯泡
中央设备可以与多个外围设备通信。 (大预览)

中央设备可以同时与多个外围设备通信,并且可以根据需要中继消息。 因此,心率监测器无法与您的灯泡通信,但是,您可以编写一个在中央设备上运行的程序,该程序接收您的心率并在心率超过某个阈值时将灯变为红色。

当我们谈论 WebBluetooth 时,我们谈论的是蓝牙规范的一个特定部分,称为 Generic Attribute Profile,它有一个非常明显的缩写 GATT。 (显然,GAP 已经被采用了。)

在 GATT 的上下文中,我们不再谈论中央设备和外围设备,而是客户端和服务器。 你的灯泡是服务器。 这可能看起来违反直觉,但如果你仔细想想,它实际上是有道理的。 灯泡提供服务,即光。 就像浏览器连接到 Internet 上的服务器一样,您的手机或计算机是一个客户端,连接到灯泡中的 GATT 服务器。

每台服务器都提供一项或多项服务。 其中一些服务是标准的正式一部分,但您也可以定义自己的服务。 对于心率监测器,规范中定义了官方服务。 就灯泡而言,没有,几乎每个制造商都试图重新发明轮子。 每个服务都有一个或多个特征。 每个特征都有一个可以读取或写入的值。 现在,最好将其视为一个对象数组,每个对象都有具有值的属性。

与 JavaScript 中更熟悉的构造相比,服务和特性的层次结构 - 服务器类似于对象数组,服务类似于该数组中的对象,特性类似于该对象的属性,并且两者都有值
服务和特征的简化层次结构。 (大预览)

与对象的属性不同,服务和特征不是由字符串标识的。 每个服务和特性都有一个唯一的 UUID,它可以是 16 位或 128 位长。 正式地,16 位 UUID 是为官方标准保留的,但几乎没有人遵循该规则。 最后,每个值都是一个字节数组。 蓝牙中没有花哨的数据类型。

近距离观察蓝牙灯泡

因此,让我们看一个实际的蓝牙设备:Mipow Playbulb Sphere。 您可以使用 BLE Scanner 或 nRF Connect 等应用程序连接到设备并查看所有服务和特征。 在这种情况下,我使用的是适用于 iOS 的 BLE Scanner 应用程序。

当您连接到灯泡时,您首先看到的是服务列表。 有一些标准化的,如设备信息服务和电池服务。 但也有一些定制服务。 我对0xff0f的 16 位 UUID 的服务特别感兴趣。 如果你打开这个服务,你可以看到一长串特征。 我不知道这些特征中的大多数是做什么的,因为它们仅由 UUID 标识,并且不幸的是它们是自定义服务的一部分; 它们没有标准化,制造商也没有提供任何文件。

UUID 为0xfffc的第一个特征似乎特别有趣。 它有四个字节的值。 如果我们将这些字节的值从0x00000000更改为0x00ff0000 ,灯泡就会变成红色。 将其更改为0x0000ff00会将灯泡变为绿色,将0x000000ff变为蓝色。 这些是 RGB 颜色,与我们在 HTML 和 CSS 中使用的十六进制颜色完全对应。

第一个字节有什么作用? 好吧,如果我们将值更改为0xff000000 ,灯泡就会变成白色。 灯泡包含四个不同的 LED,通过更改四个字节中每个字节的值,我们可以创建我们想要的每种颜色。

网络蓝牙 API

我们可以使用本机应用程序来更改灯泡的颜色真是太棒了,但是我们如何从浏览器中做到这一点呢? 事实证明,有了我们刚刚学习的蓝牙和GATT的知识,这要归功于WebBluetooth API,这还是比较简单的。 只需要几行 JavaScript 就可以改变灯泡的颜色。

让我们回顾一下 WebBluetooth API。

连接到设备

我们需要做的第一件事是从浏览器连接到设备。 我们调用函数navigator.bluetooth.requestDevice()并为函数提供一个配置对象。 该对象包含有关我们想要使用的设备以及我们的 API 应该可以使用哪些服务的信息。

在以下示例中,我们过滤设备名称,因为我们只想查看名称中包含前缀PLAYBULB的设备。 我们还将0xff0f指定为我们要使用的服务。 由于requestDevice()函数返回一个 Promise,我们可以等待结果。

 let device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix: 'PLAYBULB' } ], optionalServices: [ 0xff0f ] });

当我们调用此函数时,会弹出一个窗口,其中包含符合我们指定过滤器的设备列表。 现在我们必须手动选择要连接的设备。 这是安全和隐私的重要步骤,并为用户提供控制权。 用户决定是否允许 Web 应用程序连接,当然还决定允许连接到哪个设备。 如果用户不手动选择设备,Web 应用程序无法获取设备列表或连接。

带有用户需要用来连接设备的窗口的 Chrome 浏览器,灯泡在设备列表中可见
用户必须通过选择设备手动连接。 (大预览)

访问设备后,我们可以通过调用设备的gatt属性上的connect()函数连接到 GATT 服务器并等待结果。

 let server = await device.gatt.connect();

一旦我们有了服务器,我们就可以在服务器上调用getPrimaryService()并使用我们想要使用的服务的 UUID 作为参数并等待结果。

 let service = await server.getPrimaryService(0xff0f);

然后使用特征的 UUID 作为参数在服务上调用getCharacteristic()并再次等待结果。

我们现在有了可以用来写入和读取数据的特征:

 let characteristic = await service.getCharacteristic(0xfffc);

写入数据

要写入数据,我们可以在特征上调用函数writeValue() ,将我们要写入的值作为ArrayBuffer,这是一种二进制数据的存储方法。 我们不能使用正则数组的原因是正则数组可以包含各种类型的数据,甚至可以有空洞。

由于我们无法直接创建或修改 ArrayBuffer,因此我们使用“类型化数组”来代替。 类型化数组的每个元素总是相同的类型,并且没有任何漏洞。 在我们的例子中,我们将使用Uint8Array ,它是无符号的,因此它不能包含任何负数; 一个整数,所以它不能包含分数; 它是 8 位,只能包含 0 到 255 之间的值。换句话说:字节数组。

 characteristic.writeValue( new Uint8Array([ 0, r, g, b ]) );

我们已经知道这个特殊的灯泡是如何工作的。 我们必须提供四个字节,每个 LED 一个。 每个字节都有一个介于 0 和 255 之间的值,在这种情况下,我们只想使用红色、绿色和蓝色 LED,因此我们将白色 LED 关闭,使用值 0。

读取数据

要读取灯泡的当前颜色,我们可以使用readValue()函数并等待结果。

 let value = await characteristic.readValue(); let r = value.getUint8(1); let g = value.getUint8(2); let b = value.getUint8(3);

我们返回的值是一个 ArrayBuffer 的 DataView,它提供了一种从 ArrayBuffer 中获取数据的方法。 在我们的例子中,我们可以使用带有索引的getUint8()函数作为参数来从数组中提取单个字节。

收到更改通知

最后,还有一种方法可以在设备的值发生变化时得到通知。 这对于灯泡来说并不是很有用,但对于我们的心率监测器来说,我们的值是不断变化的,我们不想每秒钟手动轮询当前值。

 characteristic.addEventListener( 'characteristicvaluechanged', e => { let r = e.target.value.getUint8(1); let g = e.target.value.getUint8(2); let b = e.target.value.getUint8(3); } ); characteristic.startNotifications();

要在值更改时获取回调,我们必须在带有参数characteristicvaluechanged和回调函数的特征上调用addEventListener()函数。 每当值发生变化时,都会以事件对象为参数调用回调函数,我们可以从事件目标的 value 属性中获取数据。 最后,再次从 ArrayBuffer 的 DataView 中提取单个字节。

由于蓝牙网络的带宽是有限的,我们必须通过在特性上调用startNotifications()来手动启动这个通知机制。 否则,网络将被不必要的数据淹没。 此外,由于这些设备通常使用电池,因此我们不必发送的每个字节都将最终提高设备的电池寿命,因为不需要经常打开内部无线电。

结论

我们现在已经完成了 90% 以上的 WebBluetooth API。 只需几个函数调用并发送 4 个字节,您就可以创建一个控制灯泡颜色的 Web 应用程序。 如果再添加几行代码,您甚至可以控制玩具车或驾驶无人机。 随着越来越多的蓝牙设备进入市场,可能性是无穷无尽的。

更多资源

  • 蓝牙.rocks! 演示 | (GitHub上的源代码)
  • “网络蓝牙规范”,网络蓝牙社区组
  • Open GATT Registry 用于低功耗蓝牙设备的通用属性服务的非官方文档集合。