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

原文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 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 面板、玩具車、飛行器等——譯註)

擴展資源

相關文章
相關標籤/搜索