[譯] 如何在瀏覽器中編寫一款藍芽應用

西樓聽雨發表於2019-02-24

原文An Introduction To WebBluetooth
作者Niels 發表時間:february 13, 2019
譯者:西樓聽雨 發表時間: 2019/02/24 (轉載請註明出處)

這裡省略一段開篇介紹,太長,不是什麼乾貨,直接跳過不翻譯了,想看的讀者可以前往原文檢視

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.

WebBlutetooth(Web 藍芽)是一項已經在 Chrome 和 Samsung Internet (三星瀏覽器) 中被實現的新規範,它可以讓我們直接在瀏覽器中與低功耗藍芽裝置進行通訊。漸進式網頁應用配合 WebBluetooth 為可以直接與裝置進行通訊的網頁應用提供了安全保障和便利。

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.

藍芽由於它有限的輸入距離、較差的音訊質量以及配對問題,揹負了一個不好的名聲。但其實,所有這些問題都已經成為了過去。Bluetooth Low Energy (低功耗藍芽) 是一項與以往的藍芽規範沒有什麼關係的現代化的規範——除了都使用了同樣的頻段以外。每天會有超過千萬的裝置配備了藍芽,這些裝置不僅包括了手機和電腦,還包括了各種各樣的如心率、血糖監視器,還有物聯網裝置如燈泡,玩具如遙控車、飛行器等。

枯燥的理論部分

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 技術,它會用到一些對我們來說可能並不熟悉的詞彙。所以接下來我們就來看一下它是怎麼工作的以及它的一些術語。

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 phone in the middle, talking to multiple peripherals, such as a drone, a robot toy, a heart rate monitor and a lightbulb

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)。每項特性都有一個可以被讀寫的值。在現在來看,把它想象成一個物件陣列最好理解,每個物件都有自己的屬性和值。

the hierarchy of services and characteristics compared to more familiar constructs from JavaScript - a server is similar to an array of objects, a service to an object in that array, a characteristic to a property of that object and both have values

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,通過改變這四個位元組的每個的值,我們就可以製作出我們想要的所有顏色。

WebBluetooth API

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.

當我們呼叫這個函式時,會彈出一個視窗,裡面是一個滿足我們所指定的過濾條件的裝置清單。然後,我們必須從中選擇我們想要連線的裝置。這一步驟對於安全和隱私來說是不可或缺的,它把控制權交給了使用者。使用者決定了網頁應用是否可以進行連線,當然,也決定了它所允許進行連線的是哪個裝置。沒有使用者的手動選擇,網頁應用是不能獲取到裝置清單的,同樣也是無法連線的。

the Chrome browser with the window that the user needs to use to connect to a device, with the lightbulb visible in the list of devices

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 皮膚、玩具車、飛行器等——譯註)

擴充套件資源

相關文章