JavaScript 経由で Bluetooth デバイスと通信する

Web Bluetooth API を使用すると、ウェブサイトが Bluetooth デバイスと通信できます。

François Beaufort
François Beaufort

ウェブサイトが、近くにある Bluetooth デバイスと安全かつプライバシーを保護した方法で通信できるとしたらどうでしょう。これにより、心拍数モニター、歌う電球、さらにはカメもウェブサイトと直接やり取りできるようになります。

これまで、Bluetooth デバイスを操作できるのは、プラットフォーム固有のアプリに限られていました。Web Bluetooth API は、この状況を変え、ウェブブラウザにも Bluetooth を導入することを目的としています。

始める前に

このドキュメントは、Bluetooth Low Energy(BLE)と Generic Attribute Profile の仕組みに関する基本的な知識があることを前提としています。

Web Bluetooth API の仕様はまだ確定していませんが、仕様作成者は、この API を試して仕様に関するフィードバック実装に関するフィードバックを提供してくれる熱心なデベロッパーを積極的に募集しています。

Web Bluetooth API のサブセットは、ChromeOS、Android 版 Chrome 6.0、Mac(Chrome 56)、Windows 10(Chrome 70)で利用できます。つまり、近くの Bluetooth Low Energy デバイスをリクエストして接続したり、Bluetooth 特性を読み取り/書き込みしたり、GATT 通知を受信したり、Bluetooth デバイスの接続が切断されたタイミングを把握したり、Bluetooth 記述子の読み取りと書き込みを行ったりできるようになります。詳細については、MDN のブラウザの互換性の表をご覧ください。

Linux と以前のバージョンの Windows の場合は、about://flags#experimental-web-platform-features フラグを有効にします。

オリジン トライアルで利用可能

現場で Web Bluetooth API を使用しているデベロッパーからできるだけ多くのフィードバックを収集するため、Chrome では以前、ChromeOS、Android、Mac 向けのオリジン トライアルとして、Chrome 53 にこの機能を追加しました。

トライアルは 2017 年 1 月に正常に終了しました。

セキュリティ要件

セキュリティのトレードオフを理解するには、Web Bluetooth API 仕様を担当する Chrome チームのソフトウェア エンジニア、Jeffrey Yasskin による Web Bluetooth セキュリティ モデルに関する投稿をご覧ください。

HTTPS のみ

この試験運用版 API はウェブに追加された強力な新機能であるため、安全なコンテキストでのみ使用できます。つまり、TLS を念頭に置いてビルドする必要があります。

ユーザー ジェスチャーが必要

セキュリティ機能として、navigator.bluetooth.requestDevice による Bluetooth デバイスの検出は、タップやマウスクリックなどのユーザー操作によってトリガーする必要があります。pointerupclicktouchend イベントのリッスンについて説明します。

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

コードを確認する

Web Bluetooth API は、JavaScript の Promise に大きく依存しています。Promise をよくご存じない場合は、Promise のチュートリアルをご覧ください。もう 1 つ、() => {} は ECMAScript 2015 のアロー関数です。

Bluetooth デバイスをリクエストする

このバージョンの Web Bluetooth API 仕様では、中央ロールで実行されているウェブサイトが BLE 接続を介してリモート GATT サーバーに接続できます。Bluetooth 4.0 以降を実装したデバイス間の通信をサポートします。

ウェブサイトが navigator.bluetooth.requestDevice を使用して近くのデバイスへのアクセスをリクエストすると、ブラウザはデバイス選択ツールを表示します。ユーザーは、1 つのデバイスを選択するか、リクエストをキャンセルできます。

Bluetooth デバイスのユーザー プロンプト。

navigator.bluetooth.requestDevice() 関数は、フィルタを定義する必須のオブジェクトを受け取ります。これらのフィルタは、アドバタイズされた Bluetooth GATT サービスやデバイス名の一部と一致するデバイスのみを返すために使用されます。

サービス フィルタ

たとえば、Bluetooth GATT バッテリー サービスをアドバタイズしている Bluetooth デバイスをリクエストするには、次のようにします。

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

ただし、Bluetooth GATT サービスが標準化された Bluetooth GATT サービスのリストにない場合は、完全な Bluetooth UUID または短い 16 ビットまたは 32 ビット形式のいずれかを提供できます。

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

名前フィルタ

また、name フィルタキーでアドバタイズされているデバイス名、または namePrefix フィルタキーでこの名前の接頭辞に基づいて Bluetooth デバイスをリクエストすることもできます。この場合、サービス フィルタに含まれていないサービスにアクセスできるように、optionalServices キーを定義する必要もあります。これを行わないと、後でアクセスしようとするとエラーが発生します。

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

メーカーデータ フィルタ

manufacturerData フィルタキーでアドバタイズされているメーカー固有のデータに基づいて Bluetooth デバイスをリクエストすることもできます。このキーは、companyIdentifier という名前の必須の Bluetooth 企業識別子 キーを含むオブジェクトの配列です。データ接頭辞を指定して、その接頭辞で始まる Bluetooth デバイスのメーカーデータをフィルタすることもできます。サービス フィルタに含まれていないサービスにアクセスするには、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); });

マスクは、データ接頭辞とともに使用して、メーカーデータの一部パターンと照合することもできます。詳しくは、Bluetooth データフィルタの説明をご覧ください。

をご覧ください。

除外フィルタ

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 キーを使用して、近くにあるすべての Bluetooth デバイスを表示できます。また、一部のサービスにアクセスするには、optionalServices キーを定義する必要があります。これを行わないと、後でアクセスしようとしたときにエラーが発生します。

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

Bluetooth デバイスに接続する

BluetoothDevice を取得したら、次に何をすればよいですか?サービスと特性の定義を保持する Bluetooth リモート 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); });

Bluetooth 特性を読み取る

ここでは、リモート Bluetooth デバイスの GATT サーバーに接続します。次に、プライマリ GATT サービスを取得し、このサービスに属する特性を読み取ります。たとえば、デバイスのバッテリーの現在の充電レベルを読み取ってみてみましょう。

次の例では、battery_level は標準化された Battery Level Characteristic です。

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); });

カスタム Bluetooth GATT 特性を使用する場合は、完全な Bluetooth UUID または短い 16 ビットまたは 32 ビット形式を service.getCharacteristic に指定できます。

なお、特性に characteristicvaluechanged イベント リスナーを追加して、値の読み取りを処理することもできます。今後の GATT 通知を必要に応じて処理する方法については、Read Characteristic Value Changed Sample をご覧ください。


.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);
}

Bluetooth 特性に書き込む

Bluetooth GATT 特性への書き込みは、読み取りと同じくらい簡単です。今回は、心拍数コントロール ポイントを使用して、心拍数モニター デバイスの [消費カロリー] フィールドの値を 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 イベント リスナーを適切に削除する方法を示しています。

Bluetooth デバイスとの接続を解除する

ユーザー エクスペリエンスを向上させるには、切断イベントをリッスンして、ユーザーに再接続を促すことができます。

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() を呼び出して、ウェブアプリと Bluetooth デバイスの接続を解除することもできます。これにより、既存の gattserverdisconnected イベント リスナーがトリガーされます。別のアプリがすでに Bluetooth デバイスと通信している場合、Bluetooth デバイスの通信は停止されません。詳しくは、デバイスの切断サンプル自動再接続サンプルをご覧ください。

Bluetooth 記述子の読み取りと書き込み

Bluetooth GATT 記述子は、特性値を記述する属性です。Bluetooth GATT 特性と同様に読み取りと書き込みを行うことができます。

たとえば、デバイスの健康状態の温度計の測定間隔に関するユーザーの説明を読み取る方法を見てみましょう。

次の例では、health_thermometerHealth Thermometer サービスmeasurement_intervalMeasurement Interval 特性gatt.characteristic_user_descriptionCharacteristic 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); });

サンプル、デモ、Codelab

以下のウェブ Bluetooth サンプルはすべて正常にテストされています。これらのサンプルを最大限に活用するには、[BLE ペリフェラル シミュレーター Android アプリ] をインストールすることをおすすめします。このアプリは、バッテリー サービス、心拍数サービス、健康測定用温度計サービスを使用して BLE ペリフェラルをシミュレートします。

初級

  • デバイス情報 - BLE デバイスから基本的なデバイス情報を取得します。
  • バッテリー残量 - バッテリー残量をアドバタイズしている BLE デバイスからバッテリー情報を取得します。
  • エネルギーをリセット - BLE デバイスが広告する心拍数で消費されたエネルギーをリセットします。
  • Characteristic Properties - BLE デバイスの特定の特性のプロパティをすべて表示します。
  • 通知 - BLE デバイスからの特性通知を開始および停止します。
  • デバイスの切断 - BLE デバイスに接続した後に切断し、切断を通知します。
  • Get Characteristics - BLE デバイスからアドバタイズされたサービスのすべての特性を取得します。
  • Get Descriptors - BLE デバイスからアドバタイズされたサービスのすべての特性の記述子を取得します。
  • メーカーデータ フィルタ - メーカーデータと一致する BLE デバイスから基本的なデバイス情報を取得します。
  • 除外フィルタ - 基本的な除外フィルタを使用して、BLE デバイスから基本的なデバイス情報を取得します。

複数のオペレーションの組み合わせ

  • GAP 特性 - BLE デバイスのすべての GAP 特性を取得します。
  • デバイス情報の特性 - BLE デバイスのすべてのデバイス情報の特性を取得します。
  • リンク切れ - BLE デバイスのアラートレベル特性を設定します(readValue と writeValue)。
  • サービスと特性の検出 - BLE デバイスからアクセス可能なすべてのプライマリ サービスとその特性を検出します。
  • 自動再接続 - 指数バックオフ アルゴリズムを使用して、接続解除された BLE デバイスに再接続します。
  • Read Characteristic Value Changed - バッテリー残量を読み取り、BLE デバイスからの変更を通知します。
  • Read Descriptors - BLE デバイスからサービスのすべての特性の記述子を読み取ります。
  • Write Descriptor - BLE デバイスの「Characteristic User Description」デスクリプタに書き込みます。

厳選されたウェブ Bluetooth デモ公式のウェブ Bluetooth Codelab もご覧ください。

ライブラリ

  • web-bluetooth-utils は、API に便利な関数を追加する npm モジュールです。
  • Web Bluetooth API シムは、最も一般的な Node.js BLE セントラル モジュールである noble で利用できます。これにより、WebSocket サーバーやその他のプラグインを必要とせずに、noble を webpack/browserify できます。
  • angular-web-bluetooth は、Web Bluetooth API の構成に必要なすべてのボイラープレートを抽象化する Angular 用モジュールです。

ツール

  • Web Bluetooth のスタートガイドは、Bluetooth デバイスの操作を開始するための JavaScript ボイラープレート コードをすべて生成するシンプルなウェブアプリです。デバイス名、サービス、特性を入力し、そのプロパティを定義すれば、準備は完了です。
  • すでに Bluetooth デベロッパーである場合は、Web Bluetooth Developer Studio プラグインによって、Bluetooth デバイス用の Web Bluetooth JavaScript コードも生成されます。

ヒント

Chrome の [Bluetooth Internals] ページ(about://bluetooth-internals)では、近くの Bluetooth デバイスのステータス、サービス、特性、記述子など、すべてを検査できます。

Chrome で Bluetooth をデバッグするための内部ページのスクリーンショット
Bluetooth デバイスのデバッグ用に Chrome の内部ページ。

Bluetooth のデバッグは難しい場合があるため、公式の ウェブ Bluetooth バグを報告する方法のページも確認することをおすすめします。

次のステップ

まず、ブラウザとプラットフォームの実装状況を確認して、現在実装されている Web Bluetooth API の部分を確認します。

まだ完成していませんが、近い将来に予定されている機能のプレビューは次のとおりです。

  • navigator.bluetooth.requestLEScan() では、付近の BLE アドバタイズをスキャンします。
  • 新しい serviceadded イベントは、新しく検出された Bluetooth GATT サービスを追跡し、serviceremoved イベントは削除されたサービスを追跡します。新しい servicechanged イベントは、Bluetooth GATT サービスに特性や記述子が追加または削除されたときに発生します。

API のサポートを表示する

Web Bluetooth API を使用する予定はありますか?公開サポートは、Chrome チームが機能を優先付けするうえで役立ち、他のブラウザ ベンダーに、その機能をサポートすることがいかに重要であるかを示します。

ハッシュタグ #WebBluetooth を使用して @ChromiumDev にツイートを送信し、どこでどのように使用しているかをお知らせください。

リソース

謝辞

この記事を確認していただいた Kayce Basques に感謝します。 米国ボールダーの SparkFun Electronics のヒーロー画像。