原文:An Introduction To WebBluetooth
做者:Niels 发表时间:february 13, 2019
译者:西楼听雨 发表时间: 2019/02/24 (转载请注明出处)javascript
这里省略一段开篇介绍,太长,不是什么干货,直接跳过不翻译了,想看的读者能够前往原文查看java
WebBluetooth is a new specification that has been implemented in Chrome and Samsung Internet that allows us to communicate directly to Bluetooth Low Energy devices from the browser. Progressive Web Apps in combination with WebBluetooth offer the security and convenience of a web application with the power to directly talk to devices.git
WebBlutetooth(Web 蓝牙)是一项已经在 Chrome 和 Samsung Internet (三星浏览器) 中被实现的新规范,它可让咱们直接在浏览器中与低功耗蓝牙设备进行通信。渐进式网页应用配合 WebBluetooth 为能够直接与设备进行通信的网页应用提供了安全保障和便利。github
Bluetooth has a pretty bad name due to limited range, bad audio quality, and pairing problems. But, pretty much all those problems are a thing of the past. Bluetooth Low Energy is a modern specification that has little to do with the old Bluetooth specifications, apart from using the same frequency spectrum. More than 10 million devices ship with Bluetooth support every single day. That includes computers and phones, but also a variety of devices like heart rate and glucose monitors, IoT devices like light bulbs and toys like remote controllable cars and drones.web
蓝牙因为它有限的输入距离、较差的音频质量以及配对问题,背负了一个很差的名声。但其实,全部这些问题都已经成为了过去。Bluetooth Low Energy (低功耗蓝牙) 是一项与以往的蓝牙规范没有什么关系的现代化的规范——除了都使用了一样的频段之外。天天会有超过千万的设备配备了蓝牙,这些设备不只包括了手机和电脑,还包括了各类各样的如心率、血糖监视器,还有物联网设备如灯泡,玩具如遥控车、飞行器等。vim
Since Bluetooth itself is not a web technology, it uses some vocabulary that may seem unfamiliar to us. So let’s go over how Bluetooth works and some of the terminology.数组
因为蓝牙自己并非一项 Web 技术,它会用到一些对咱们来讲可能并不熟悉的词汇。因此接下来咱们就来看一下它是怎么工做的以及它的一些术语。promise
Every Bluetooth device is either a ‘Central device’ or a ‘Peripheral’. Only central devices can initiate communication and can only talk to peripherals. An example of a central device would be a computer or a mobile phone.浏览器
每一个蓝牙设备,要么是“中心设备”,要么是“外围设备”。只有中心设备才能够发起通信,并且只能与外围设备进行通信。电脑和手机就是中心设备的一个例子。安全
A peripheral cannot initiate communication and can only talk to a central device. Furthermore, a peripheral can only talk to one central device at the same time. A peripheral cannot talk to another peripheral.
外围设备是不能发起通信的,也只能与中心设备进行通信;并且,外围设备在同一时间只能与一个中心设备通信。外围设备不能与另外一个外围设备进行通信。
A central device can talk to multiple peripherals at the same time and could relay messages if it wanted to. So a heart rate monitor could not talk to your lightbulbs, however, you could write a program that runs on a central device that receives your heart rate and turns the lights red if the heart rate gets above a certain threshold.
中心设备能够与多个外围设备同时通信,也能够对消息进行中继。因此,虽然心率监控器不能与你的灯泡通信,可是,你能够编写一个运行在中心设备上的程序,让他接收你的心率并在心率达到特定阈值时将灯光变红。
When we talk about WebBluetooth, we are talking about a specific part of the Bluetooth specification called Generic Attribute Profile, which has the very obvious abbreviation GATT. (Apparently, GAP was already taken.)
当咱们在谈论 WebBluetooth 时,其实咱们谈论的是蓝牙规范中的一个特定的叫作 Generic Attribute Profile (通用属性协议——译注) 的部分,简称 GATT (貌似是由于 GAP 已经被占用而这样简称)
In the context of GATT, we are no longer talking about central devices and peripherals, but clients and servers. Your light bulbs are servers. That may seem counter-intuitive, but it actually makes sense if you think about it. The light bulb offers a service, i.e. light. Just like when the browser connects to a server on the Internet, your phone or computer is a client that connects to the GATT server in the light bulb.
在 GATT 的语境下,咱们再也不称中心设备和外围设备了,而是改称为客户端和服务端。你的灯泡就是服务端,这看上去有点反直觉,但若是你认真思考一下就会发现这其实是有其意义的。灯泡提供了一项服务,即“光”,就像浏览器链接到服务器同样,你的手机或电脑就是一个链接到了这个灯泡里的 GATT 服务端的客户端。
Each server offers one or more services. Some of those services are officially part of the standard, but you can also define your own. In the case of the heart rate monitor, there is an official service defined in the specification. In case of the light bulb, there is not, and pretty much every manufacturer tries to re-invent the wheel. Every service has one or more characteristics. Each characteristic has a value that can be read or written. For now, it would be best to think of it as an array of objects, with each object having properties that have values.
每一个服务端能够提供一项或多项服务。这些服务,有些是属于官方标准的一部分,但你也能够定义属于你本身的服务。对于心率监视器来讲,已经有一项官方的服务在规范中存在了;而对于灯泡来讲,尚未,因此几乎全部厂商都会尝试“重复造轮子”。每项服务又有一个或多个特性(characteristic)。每项特性都有一个能够被读写的值。在如今来看,把它想象成一个对象数组最好理解,每一个对象都有本身的属性和值。
Unlike properties of objects, the services and characteristics are not identified by a string. Each service and characteristic has a unique UUID which can be 16 or 128 bits long. Officially, the 16 bit UUID is reserved for official standards, but pretty much nobody follows that rule. Finally, every value is an array of bytes. There are no fancy data types in Bluetooth.
和对象的属性不同,服务项和特性不是用字符串来标识的。每项服务和每一个特性都有一个 16 或 128 位比特长的惟一的 UUID。官方规定,16 比特的 UUID 用来保留在各项官方标准上,但几乎没有人遵照这项规定。最后要说的就是,每一个特性值都是一个字节数组——在蓝牙中没有所谓的什么数据类型。
So let’s look at an actual Bluetooth device: a Mipow Playbulb Sphere. You can use an app like BLE Scanner, or nRF Connect to connect to the device and see all the services and characteristics. In this case, I am using the BLE Scanner app for iOS.
下面咱们来看一下一个真实的蓝牙设备:一台 Mipow 牌的灯光球。你可使用 BLE Scanner 或者 nRF Connect 这类 APP 来链接这台设备并查看它的全部服务项和特性。这里我使用的是 BLE Scanner 应用的 iOS 版。
视频演示地址(需越墙):vimeo.com/303046505
The first thing you see when you connect to the light bulb is a list of services. There are some standardized ones like the device information service and the battery service. But there are also some custom services. I am particularly interested in the service with the 16 bit UUID of 0xff0f
. If you open this service, you can see a long list of characteristics. I have no idea what most of these characteristics do, as they are only identified by a UUID and because they are unfortunately a part of a custom service; they are not standardized, and the manufacturer did not provide any documentation.
当你链接到这个灯泡时,第一眼看到的是一个服务项清单。里面有一些是标准化的服务项,如设备信息(device Information)服务项和电池信息服务项;不过也有一些是自定义的服务项。我特别感兴趣的是那项 16 比特长的 UUID 的值为 0xff0f
的服务项。若是你点开这项服务项的话,你会看到一个长长的特性清单;这些特性的大部分我都不知道是什么,由于他们只有 UUID,并且更加遗憾的他们归属于自定义服务项;他们没有被标准化,厂商也没有提供任何文档。
The first characteristic with the UUID of 0xfffc
seems particularly interesting. It has a value of four bytes. If we change the value of these bytes from 0x00000000
to 0x00ff0000
, the light bulb turns red. Changing it to 0x0000ff00
turns the light bulb green, and 0x000000ff
blue. These are RGB colors and correspond exactly to the hex colors we use in HTML and CSS.
第一个特性的 UUID 为 0xfffc
,看起来特别有趣,它的值是4个字节,若是咱们把这些字节从 0x00000000
改成 0x00ff0000
,灯泡就会变红;改成 0x0000ff00
则会变绿,0x000000ff
变蓝。这些都是 RGB 颜色,恰好与咱们在 HTML 和 CSS 中使用的十六进制的颜色对应。
What does that first byte do? Well, if we change the value to 0xff000000
, the lightbulb turns white. The lightbulb contains four different LEDs, and by changing the value of each of the four bytes, we can create every single color we want.
那么第一个字节是用来干吗的呢?嗯,若是咱们把值改成 0xff000000
,灯泡就会变白。灯泡里有四个不一样的 LED,经过改变这四个字节的每一个的值,咱们就能够制做出咱们想要的全部颜色。
It is fantastic that we can use a native app to change the color of a light bulb, but how do we do this from the browser? It turns out that with the knowledge about Bluetooth and GATT we just learned, this is relatively simple thanks to the WebBluetooth API. It only takes a couple of lines of JavaScript to change the color of a light bulb.
用本地应用来改变灯泡的颜色是极其可行的,但若是是放在浏览器里面来作,咱们该怎么作呢?刚刚咱们已经学习了蓝牙和 GATT 相关的知识,借助于 WebBluetooth API 。只须要几行 JS 代码就能够改变灯泡的颜色。
Let’s go over the WebBluetooth API.
下面咱们就来看下 WebBluetooth API。
The first thing we need to do is to connect from the browser to the device. We call the function navigator.bluetooth.requestDevice()
and provide the function with a configuration object. That object contains information about which device we want to use and which services should be available to our API.
咱们须要作的第一件事就是,在浏览器中与那台设备进行链接。调用函数 navigator.bluetooth.requestDevice()
,并传入一个配置对象,这个对象包含了关于咱们想要使用的设备和服务的信息。
In the following example, we are filtering on the name of the device, as we only want to see devices that contain the prefix PLAYBULB
in the name. We are also specifying 0xff0f
as a service we want to use. Since the requestDevice()
function returns a promise, we can await the result.
在下面这个例子中,咱们基于设备的名字进行了筛选,由于咱们只但愿看到名字中包含了 PLAYBULB
前缀的设备;咱们还用 0xff0f
来指定了咱们想使用的服务项。因为 requestDevice()
函数返回的是一个 promise,因此咱们能够 await 它的结果。
let device = await navigator.bluetooth.requestDevice({
filters: [
{ namePrefix: 'PLAYBULB' }
],
optionalServices: [ 0xff0f ]
});
复制代码
When we call this function, a window pops up with the list of devices that conform to the filters we’ve specified. Now we have to select the device we want to connect to manually. That is an essential step for security and privacy and gives control to the user. The user decides whether the web app is allowed to connect, and of course, to which device it is allowed to connect. The web app cannot get a list of devices or connect without the user manually selecting a device.
当咱们调用这个函数时,会弹出一个窗口,里面是一个知足咱们所指定的过滤条件的设备清单。而后,咱们必须从中选择咱们想要链接的设备。这一步骤对于安全和隐私来讲是不可或缺的,它把控制权交给了用户。用户决定了网页应用是否能够进行链接,固然,也决定了它所容许进行链接的是哪一个设备。没有用户的手动选择,网页应用是不能获取到设备清单的,一样也是没法链接的。
After we get access to the device, we can connect to the GATT server by calling the connect()
function on the gatt
property of the device and await the result.
在咱们获取到这台设备后,我让就能够经过调用这个设备的 gatt
属性上的 connect
()` 函数来链接到 GATT 服务端上,并 await 它的结果。
let server = await device.gatt.connect();
复制代码
Once we have the server, we can call getPrimaryService()
on the server with the UUID of the service we want to use as a parameter and await the result.
得到服务端后,咱们就能够用咱们想要使用的服务项的 UUID 做为参数来调用它的 getPrimaryService()
,并 await 其结果。
let service = await server.getPrimaryService(0xff0f);
复制代码
Then call getCharacteristic()
on the service with the UUID of the characteristic as a parameter and again await the result.
而后再在服务项上用特性的 UUID 做为参数来调用 getCharacteristic()
,而后继续 await 其结果。
We now have our characteristics which we can use to write and read data:
而后获得了咱们的特性以后,咱们就能够用它来读写数据了:
let characteristic = await service.getCharacteristic(0xfffc);
复制代码
To write data, we can call the function writeValue()
on the characteristic with the value we want to write as an ArrayBuffer, which is a storage method for binary data. The reason we cannot use a regular array is that regular arrays can contain data of various types and can even have empty holes.
想要写入数据,咱们能够把咱们想要写入的值做为一个 ArrayBuffer 来在特性上调用 writeValue()
函数——ArrayBuffer 是一种二进制数据的存储方式。咱们不使用常规数组的缘由是数组能够包含任意类型的数据,并且甚至可能存在“空洞”。
Since we cannot create or modify an ArrayBuffer directly, we are using a ‘typed array’ instead. Every element of a typed array is always the same type, and it does not have any holes. In our case, we are going to use a Uint8Array
, which is unsigned so it cannot contain any negative numbers; an integer, so it cannot contain fractions; and it is 8 bits and can contain only values from 0 to 255. In other words: an array of bytes.
因为咱们不能直接建立和修改 ArrayBuffer,咱们须要改用“typed array” (类型化数组) 来实现——Typed Array 中的全部元素都是相同的类型,也没有任何“空洞”。在咱们的这个例子中,咱们将使用的是 Unit8Array
,它是无符号的整型,因此不会包含任何负数和小数部分;同时他仍是 8 比特长的,因此只能包含 0~255。换言之:它就是一个字节数组。
characteristic.writeValue(
new Uint8Array([ 0, r, g, b ])
);
复制代码
We already know how this particular light bulb works. We have to provide four bytes, one for each LED. Each byte has a value between 0 and 255, and in this case, we only want to use the red, green and blue LEDs, so we leave the white LED off, by using the value 0.
咱们已经知道这个灯泡是如何工做的了。咱们须要提供四个字节,对应到各个 LED。每一个字节的值,范围为 0~255,在这个例子中,咱们想要使用到的只有红、绿、蓝 LED ,因此咱们经过使用 0 来保持白色 LED 关闭。
To read the current color of the light bulb, we can use the readValue()
function and await the result.
咱们可使用 readValue()
函数来读取灯泡当前的颜色,并 await 它的结果。
let value = await characteristic.readValue();
let r = value.getUint8(1);
let g = value.getUint8(2);
let b = value.getUint8(3);
复制代码
The value we get back is a DataView of an ArrayBuffer, and it offers a way to get the data out of the ArrayBuffer. In our case, we can use the getUint8()
function with an index as a parameter to pull out the individual bytes from the array.
咱们取回来的值是一个 ArrayBuffer 的 DataView (数据视图),它提供了一种从 ArrayBuffer 取出数据的方式。在咱们的例子中,咱们能够经过将一个下标做为参数来使用 getUint8()
函数拉取单个字节。
Finally, there is also a way to get notified when the value of a device changes. That isn’t really useful for a lightbulb, but for our heart rate monitor we have constantly changing values, and we don’t want to poll the current value manually every single second.
最后,还有一种方式是在设备的值发生变更了得到通知。对于灯泡来讲,这个其实真的没什么用,但对于咱们的心率监视器来讲,它的值是持续不断变化的,咱们不但愿手动每秒来获取当前的值。
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();
复制代码
To get a callback whenever a value changes, we have to call the addEventListener()
function on the characteristic with the parameter characteristicvaluechanged
and a callback function. Whenever the value changes, the callback function will be called with an event object as a parameter, and we can get the data from the value property of the target of the event. And, finally extract the individual bytes again from the DataView of the ArrayBuffer.
要想在值发生变更时得到回调,咱们须要在特性上调用 addEventListener()
函数——使用 characteristicvaluechanged
和一个回调函数做为参数。这样,在值发生变更时,回调函数就会被调用,并接受到一个 event 对象,咱们能够从这个 event 的 target 属性的 value 属性上得到数据,而后再经过 ArrayBuffer 的 DataView 提取各个字节。
Because the bandwidth on the Bluetooth network is limited, we have to manually start this notification mechanism by calling startNotifications()
on the characteristic. Otherwise, the network is going to be flooded by unnecessary data. Furthermore, because these devices typically use a battery, every single byte that we do not have to send will definitively improve the battery life of the device because the internal radio does not need to be turned on as often.
因为蓝牙网络的带宽有限,咱们必须手动调用 startNotifications()
来启动通知机制;不然,网络中就会充斥着不必的数据。而后,因为这些设备一般会用到一个电池,因此每节省一个不必发送的字节,均可以提高设备的电池续航,由于不必常常性地打开内部的射频信号。
We’ve now gone over 90% of the WebBluetooth API. With just a few function calls and sending 4 bytes, you can create a web app that controls the colors of your light bulbs. If you add a few more lines, you can even control a toy car or fly a drone. With more and more Bluetooth devices making their way on to the market, the possibilities are endless.
咱们已经对 WebBluetooth API 作了 90% 的讲解了。只需调用几个函数,发送4个字节,你就能够建立一个能控制你灯泡颜色的网页应用。若是再多写几行代码,你甚至能够控制一台玩具车或者飞起一台飞行器。随着愈来愈多的蓝牙设备不断地进入市场,将来将有无限的可能。
视频演示地址(需越墙):vimeo.com/303045191
(这个视频里演示了经过网页来控制彩灯、LED 面板、玩具车、飞行器等——译注)
Bluetooth.rocks! Demos (以上项目的示例程序)| (Source code on GitHub) (及其源代码)
“Web Bluetooth Specification,” Web Bluetooth Community Group 蓝牙规范文档
Open GATT Registry 开放性 GATT 登记表 An unofficial collection of documentation for Generic Attribute services for Bluetooth Low Energy devices.
一份非官方性质的针对微型蓝牙设备的 Generic Attribute 服务文档集合。