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

Web Bluetooth API を使用すると、ウェブサイトと Bluetooth デバイス間の通信が可能になります。

François Beaufort
François Beaufort

ウェブサイトがプライバシーに配慮した安全な方法で付近の Bluetooth デバイスと通信できるとしたらどうしますか?このようにして、心拍数モニターをしたり、電球を鳴らしたり、さらにはカメまでもがウェブサイトと直接やり取りできるようになりました。

これまで Bluetooth デバイスの操作は、プラットフォーム固有のアプリでのみ可能でした。Web Bluetooth API は、これを変更し、ウェブブラウザにも導入することを目指しています。

始める前に

このドキュメントは、Bluetooth Low Energy(BLE)と汎用属性プロファイルの仕組みに関する基本的な知識があることを前提としています。

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

Web Bluetooth API のサブセットは、ChromeOS、Chrome for Android 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 では以前、この機能を Chrome 53 に ChromeOS、Android、Mac 向けのオリジン トライアルとして追加しました。

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

セキュリティ要件

セキュリティのトレードオフを理解するには、ウェブ Bluetooth API 仕様に取り組む Chrome チームのソフトウェア エンジニアである Jeffrey Yasskin による Web Bluetooth Security Model の投稿をご覧ください。

HTTPS のみ

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

必要なユーザー操作

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

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

コードに取り組む

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

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

このバージョンの Web Bluetooth API 仕様では、Central ロールで実行されているウェブサイトが 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 フィルタキーを使用して Bluetooth デバイスをリクエストすることもできます。また、この名前の接頭辞に 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 フィルタキーを使用して、アドバタイズされているメーカー固有のデータに基づいて Bluetooth デバイスをリクエストすることもできます。このキーは、必須の Bluetooth 会社識別子キー companyIdentifier を持つオブジェクトの配列です。メーカーのデータを、そのデバイスで始まる 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標準化されたバッテリー レベル特性です。

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 通知も任意で処理する方法を確認してください。

…
.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 特性への書き込みは、読むのと同じくらい簡単です。今度は、心拍数コントロール ポイントを使用して、心拍数モニター デバイスの [Energy Expended] フィールドの値を 0 にリセットしましょう。

魔法などないことを約束します。詳しくは、Heart Rate Control Point Characteristic ページをご覧ください。

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_thermometer体温計サービスmeasurement_intervalMeasurement Interval characteristicgatt.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 周辺機器をシミュレートする [BLE Peripheral Simulator Android App] をインストールすることをおすすめします。

初級

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

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

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

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

ライブラリ

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

ツール

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

ヒント

Bluetooth の内部ページは Chrome の about://bluetooth-internals で利用できます。このページにより、ステータス、サービス、特性、記述子など、付近の Bluetooth デバイスに関するあらゆる情報を調べることができます。

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

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

次のステップ

最初にブラウザとプラットフォームの実装ステータスを確認し、Web Bluetooth API のどの部分が現在実装されているかを把握します。

まだ不完全ですが、近い将来に想定されることを以下に紹介します。

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

API のサポートを表示する

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

ハッシュタグ #WebBluetooth を使用して @ChromiumDev 宛てにツイートを送信し、使用場所と使用方法をお知らせください。

リソース

謝辞

この記事をレビューしてくれた Kayce Basques に感謝します。ヒーロー画像(SparkFun Electronics(米国、ボルダー)