透過 JavaScript 與藍牙裝置通訊

Web Bluetooth API 可讓網站與藍牙裝置通訊。

François Beaufort
François Beaufort

如果我告訴網站能以安全且保護隱私權的方式與附近的藍牙裝置通訊,該怎麼辦?這樣一來,心率監測器、歌聲燈泡,甚至是烏龜都能直接與網站互動。

目前為止,只有平台專用應用程式才能與藍牙裝置互動。Web Bluetooth API 的目的在於改變這一點,並將新格式提供給網路瀏覽器。

課前說明

本文假設您對藍牙低功耗 (BLE) 和一般屬性設定檔的運作方式有一定程度的瞭解。

即使 Web Bluetooth API 規格尚未定案,規格作者也積極尋找充滿熱忱的開發人員試用這個 API,並向我們提供規格對規格的意見回饋有關實作方式的意見回饋

ChromeOS、Android 6.0、Mac 版 Google Chrome (Chrome 56) 和 Windows 10 (Chrome 70) 皆提供一部分 Web Bluetooth API。這表示您應該能夠要求連線至附近的藍牙低功耗裝置、讀取/寫入藍牙特徵、接收 GATT 通知、瞭解藍牙裝置連線中斷情形,甚至是讀取及寫入藍牙描述元。詳情請參閱 MDN 的「瀏覽器相容性」表格。

如果是 Linux 和舊版 Windows,請在 about://flags 中啟用 #experimental-web-platform-features 旗標。

適用於來源試用

為盡可能從使用 Web Bluetooth API 的開發人員獲得充分的回饋,Chrome 先前已在 Chrome 53 中增加這項功能,做為 ChromeOS、Android 和 Mac 的「來源試用」

試用期已經順利在 2017 年 1 月結束。

安全性相關規定

為了瞭解安全性的取捨,建議您閱讀 Chrome 團隊軟體工程師 Jeffrey Yasskin 所撰寫的網路藍牙安全性模型文章,目前使用 Web Bluetooth API 規格。

僅限 HTTPS

這個實驗性 API 是新增至網路的強大新功能,因此僅供安全環境使用。這表示您需要在建構時注意傳輸層安全標準 (TLS)

需要使用者手勢

安全性功能是,透過 navigator.bluetooth.requestDevice 探索藍牙裝置必須由使用者手勢 (例如輕觸或點擊滑鼠) 觸發。我們將說明監聽 pointerupclicktouchend 事件。

button.addEventListener('pointerup', function(event) {
  // Call navigator.bluetooth.requestDevice
});

取得程式碼

Web Bluetooth API 非常仰賴 JavaScript Promise。如果您不熟悉這些內容,請參考這個實用的 Promise 教學課程。另外,() => {} 是 ECMAScript 2015 箭頭函式

要求藍牙裝置

這個版本的 Web Bluetooth API 規格可讓以 Central 角色執行的網站,透過 BLE 連線連線至遠端 GATT 伺服器。支援執行藍牙 4.0 以上版本的裝置之間的通訊。

當網站使用 navigator.bluetooth.requestDevice 要求存取鄰近裝置時,瀏覽器會提示使用者挑選一部裝置或取消要求。

藍牙裝置使用者提示。

navigator.bluetooth.requestDevice() 函式會使用定義篩選器的必要物件。這些篩選器僅用於傳回符合部分宣傳藍牙 GATT 服務和/或裝置名稱的裝置。

服務篩選器

舉例來說,如要要求藍牙裝置宣傳藍牙 GATT 電池服務,請按照下列步驟操作:

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => { /* … */ })
.catch(error => { console.error(error); });

如果您的藍牙 GATT 服務不在標準化藍牙 GATT 服務清單中,您可以提供完整的藍牙 UUID,或是簡短的 16 或 32 位元格式。

navigator.bluetooth.requestDevice({
  filters: [{
    services: [0x1234, 0x12345678, '99999999-0000-1000-8000-00805f9b34fb']
  }]
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

名稱篩選器

您也可以根據使用 name 篩選器鍵宣傳的裝置名稱,或甚至在名稱前加上 namePrefix 篩選器金鑰的名稱,來要求藍牙裝置。請注意,在這種情況下,您還需要定義 optionalServices 金鑰,才能存取服務篩選器未包含的任何服務。否則,之後嘗試存取時就會收到錯誤訊息。

navigator.bluetooth.requestDevice({
  filters: [{
    name: 'Francois robot'
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

製造商資料篩選器

您也可以透過 manufacturerData 篩選器鍵宣傳的製造商特定資料,要求藍牙裝置。這個金鑰是含有必要藍牙公司 ID 金鑰的物件陣列,該金鑰名為 companyIdentifier。您也可以提供資料前置字串,藉此篩選製造商資料,從這類資料啟動者的藍牙裝置資料。請注意,您也必須定義 optionalServices 金鑰,才能存取服務篩選器未包含的任何服務。否則之後嘗試存取時,您將收到錯誤訊息。

// Filter Bluetooth devices from Google company with manufacturer data bytes
// that start with [0x01, 0x02].
navigator.bluetooth.requestDevice({
  filters: [{
    manufacturerData: [{
      companyIdentifier: 0x00e0,
      dataPrefix: new Uint8Array([0x01, 0x02])
    }]
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

遮罩也可以搭配資料前置字串使用,以比對製造商資料中的部分模式。詳情請參閱藍牙資料篩選器說明

排除篩選器

navigator.bluetooth.requestDevice() 中的 exclusionFilters 選項可讓您將部分裝置從瀏覽器挑選器中排除。可用於排除符合更廣泛篩選條件但不支援的裝置。

// Request access to a bluetooth device whose name starts with "Created by".
// The device named "Created by Francois" has been reported as unsupported.
navigator.bluetooth.requestDevice({
  filters: [{
    namePrefix: "Created by"
  }],
  exclusionFilters: [{
    name: "Created by Francois"
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

不要使用濾鏡

最後,如果不使用 filters,您也可以使用 acceptAllDevices 鍵,顯示所有附近的藍牙裝置。此外,您也需要定義 optionalServices 金鑰才能存取部分服務。否則之後嘗試存取時就會收到錯誤訊息。

navigator.bluetooth.requestDevice({
  acceptAllDevices: true,
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

連線到藍牙裝置

那麼,有了 BluetoothDevice 後,你要做什麼呢?讓我們連線至含有服務和特性定義的藍牙遠端 GATT 伺服器。

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
  // Human-readable name of the device.
  console.log(device.name);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

讀取藍牙特性

我們在此連接到遠端藍牙裝置的 GATT 伺服器。現在,我們要取得主要 GATT 服務,並讀取屬於這項服務的特性。舉例來說,讓我們嘗試讀取裝置電池目前的電量。

在前面的範例中,battery_level標準化電池電量特性

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => device.gatt.connect())
.then(server => {
  // Getting Battery Service…
  return server.getPrimaryService('battery_service');
})
.then(service => {
  // Getting Battery Level Characteristic…
  return service.getCharacteristic('battery_level');
})
.then(characteristic => {
  // Reading Battery Level…
  return characteristic.readValue();
})
.then(value => {
  console.log(`Battery percentage is ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });

如果您使用自訂藍牙 GATT 特性,可以將完整藍牙 UUID 或簡短的 16 位元或 32 位元格式提供給 service.getCharacteristic

請注意,您也可以在特性中新增 characteristicvaluechanged 事件監聽器來處理讀取其值。請參閱讀取特性值已變更範例,瞭解如何選擇性處理即將進行的 GATT 通知。

…
.then(characteristic => {
  // Set up event listener for when characteristic value changes.
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleBatteryLevelChanged);
  // Reading Battery Level…
  return characteristic.readValue();
})
.catch(error => { console.error(error); });

function handleBatteryLevelChanged(event) {
  const batteryLevel = event.target.value.getUint8(0);
  console.log('Battery percentage is ' + batteryLevel);
}

寫入藍牙特性

寫入藍牙 GATT 特性就和讀出一樣簡單。這次我們會使用心率控制點,將心率監測器上的 Energy Expended 欄位值重設為 0。

我保證這沒有魔法。如需相關資訊,請參閱心率控制點特性頁面

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_control_point'))
.then(characteristic => {
  // Writing 1 is the signal to reset energy expended.
  const resetEnergyExpended = Uint8Array.of(1);
  return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
  console.log('Energy expended has been reset.');
})
.catch(error => { console.error(error); });

接收 GATT 通知

現在,我們說明如何在裝置的心率測量功能變更時接收通知:

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => characteristic.startNotifications())
.then(characteristic => {
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleCharacteristicValueChanged);
  console.log('Notifications have been started.');
})
.catch(error => { console.error(error); });

function handleCharacteristicValueChanged(event) {
  const value = event.target.value;
  console.log('Received ' + value);
  // TODO: Parse Heart Rate Measurement value.
  // See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}

通知範例說明如何使用 stopNotifications() 停止通知,並正確移除新增的 characteristicvaluechanged 事件監聽器。

中斷與藍牙裝置的連線

如要提供更優質的使用者體驗,建議您監聽中斷連線事件,並邀請使用者重新連線:

navigator.bluetooth.requestDevice({ filters: [{ name: 'Francois robot' }] })
.then(device => {
  // Set up event listener for when device gets disconnected.
  device.addEventListener('gattserverdisconnected', onDisconnected);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

function onDisconnected(event) {
  const device = event.target;
  console.log(`Device ${device.name} is disconnected.`);
}

您也可以呼叫 device.gatt.disconnect(),中斷網頁應用程式與藍牙裝置的連線。這會觸發現有的 gattserverdisconnected 事件監聽器。請注意,如果其他應用程式已經與藍牙裝置通訊,則不會停止藍牙裝置的通訊。詳情請參閱裝置中斷連線範例自動重新連線範例

讀取及寫入藍牙描述元

藍牙 GATT 描述元是用來描述特性值的屬性。讀取及寫入這類字元的方式與藍牙 GATT 特性類似。

讓我們看看如何讀取裝置健康溫度計測量間隔的使用者說明。

在以下範例中,health_thermometer健康溫度計服務measurement_interval評估間隔特性,以及 gatt.characteristic_user_description特性使用者說明描述元

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => descriptor.readValue())
.then(value => {
  const decoder = new TextDecoder('utf-8');
  console.log(`User Description: ${decoder.decode(value)}`);
})
.catch(error => { console.error(error); });

現在介紹了裝置健康溫度計測量間隔的使用者說明,讓我們來看看如何更新並寫入自訂值。

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => {
  const encoder = new TextEncoder('utf-8');
  const userDescription = encoder.encode('Defines the time between measurements.');
  return descriptor.writeValue(userDescription);
})
.catch(error => { console.error(error); });

範例、示範與程式碼研究室

下列所有網路藍牙範例皆已成功測試。為了充分發揮這些範例的效果,建議您安裝 [BLE 週邊裝置模擬器 Android 應用程式],透過電池服務、心率服務或健康溫度計服務模擬 BLE 週邊裝置。

Beginner

  • 裝置資訊:從 BLE 裝置擷取基本裝置資訊。
  • 電池電量:透過 BLE 裝置廣告電池資訊擷取電池資訊。
  • 重設能量 - 重設 BLE 裝置廣告心率所消耗的能量。
  • 特色屬性 - 顯示 BLE 裝置中特定特性的所有屬性。
  • 通知 - 開始和停止透過 BLE 裝置傳送特性通知。
  • 裝置中斷連線:在與 BLE 裝置連線後中斷連線並收到相關通知。
  • 掌握特色 - 運用 BLE 裝置取得廣告宣傳服務的所有特性。
  • 取得描述元 - 從 BLE 裝置取得廣告服務的所有特性描述元。
  • 製造商資料篩選器 - 從與製造商資料相符的 BLE 裝置擷取基本裝置資訊。
  • 排除篩選器:透過設有基本排除篩選器的 BLE 裝置擷取基本裝置資訊。

合併多項作業

另請參考我們精選的 Web 藍牙示範官方網路藍牙程式碼研究室

程式庫

  • web-bluetooth-utils 是一個 npm 模組,可以為 API 新增一些便利函式。
  • noble 為最熱門的 Node.js BLE 中央模組,提供 Web Bluetooth API 填充碼。這使您無需使用 WebSocket 伺服器或其他外掛程式,即可進行 webpack/browserify,
  • angular-web-bluetoothAngular 的模組,可讓您捨棄設定 Web Bluetooth API 所需的所有樣板。

工具

  • 開始使用網路藍牙」是簡單的網頁應用程式,可產生所有 JavaScript 樣板程式碼,以便開始與藍牙裝置互動。只要輸入裝置名稱、服務或特性,並定義其屬性即可。
  • 如果您是藍牙開發人員,網路藍牙開發人員 Studio 外掛程式也會為藍牙裝置產生網路藍牙 JavaScript 程式碼。

提示

您可以在 Chrome 的 about://bluetooth-internals 查看藍牙內部頁面,以便檢查鄰近藍牙裝置的所有相關資訊,包括狀態、服務、特性和描述元。

用於對 Chrome 藍牙偵錯的內部網頁螢幕截圖
用於為藍牙裝置偵錯的 Chrome 內部頁面。

此外,也建議您參閱官方如何提出網路藍牙錯誤頁面,因為藍牙偵錯作業有時並不容易。

後續步驟

請先查看瀏覽器和平台實作狀態,瞭解目前正在實作 Web Bluetooth API 的部分。

雖然步驟並不完整,但以下簡要說明未來的計劃:

  • 系統會使用 navigator.bluetooth.requestLEScan() 掃描附近的 BLE 廣告
  • 新的 serviceadded 事件會追蹤新發現的藍牙 GATT 服務,而 serviceremoved 事件則會追蹤已移除的藍牙 GATT 服務。從藍牙 GATT 服務新增或移除任何特性和/或描述元時,就會觸發新的 servicechanged 事件。

展現對 API 的支援

你打算使用 Web Bluetooth API 嗎?您的公開支援可協助 Chrome 團隊決定功能的優先順序,讓其他瀏覽器廠商瞭解這項功能有多重要。

請使用主題標記 #WebBluetooth 將 Tweet 訊息傳送至 @ChromiumDev,告訴我們您的使用地點和方式。

資源

特別銘謝

感謝 Kayce Basques 協助審查這篇文章。 主頁橫幅由 美國博爾德的《SparkFun Electronics》發布。