Web Bluetoothモジュール for Angular

ここ数ヶ月、私は新しいWeb Bluetooth APIを試してきました。これは2017年2月にChrome 56で提供される予定です。この新機能は、Webの新たな可能性を切り開いてくれました。

Web Advocateとして、私はこの機能にとても興奮し、AngularとWeb Bluetooth APIを組み合わせる方法を示すアプリケーションを作りたくてたまりませんでした(さらに、今後のWeb APIでも同様です。詳細は後日お楽しみに)。

AngularアプリケーションのためのWeb Bluetoothモジュール

それから、François Beaufort(彼に感謝!)と一緒に、Web BluetoothとAngularの統合を示すデモアプリケーションを作成しました。これは、Web BluetoothをAngularと統合する方法を示す概念実証の一部です。

いくつかのユースケースを実装した後、私はWeb Bluetooth APIを設定するためのボイラープレートを抽象化したAngularモジュールを作成しました。

注意事項

Web Bluetooth API

ここでは、Web Bluetooth API(GATTサーバー、サービス、特性など)についてすでに理解していると仮定します。次のセクションを読む前に、以下のリソースでこのトピックに慣れてください:

  1. https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web
  2. https://medium.com/@urish/start-building-with-web-bluetooth-and-progressive-web-apps-6534835959a6

Observables

また、Observables(Observable、Observer、Subject)について基本的な知識があると仮定しています。以下のリソースで理解を深めてください:

フィンランド記法

いくつかのメソッドの末尾に$記号が付いていることに気付くでしょう。これはObservablesの世界で使用されている慣習です。今後、このブログポストにより、$記号を削除する可能性もあります。

https://miro.medium.com/v2/resize:fit:1000/1*hTkqd86iz_wxMBNT74EGVA.gif

モジュールのインストール

このモジュールは、Yarn または NPM を使用して取得できます:

$ yarn add @manekinekko/angular-web-bluetooth$ npm i -S @manekinekko/angular-web-bluetooth

WebBluetoothModuleの使用

このモジュールは簡単に使用できます。まず、WebBluetoothModule モジュールを @manekinekko/angular-web-bluetooth からインポートします:

import { NgModule } from '@angular/core';
import { 
  WebBluetoothModule
} from '@manekinekko/angular-web-bluetooth';
@NgModule({
  imports: [
    //...,
    WebBluetoothModule.forRoot()
  ],
  //...
})
export class AppModule { }

WebBluetoothModule.forRoot() メソッドを呼び出すことで、BluetoothCore サービスが提供され、これを自分のサービスやコンポーネントで使用する必要があります。例えば、battery-level.component.ts での使用方法は以下の通りです:

import { Component, OnInit, NgZone } from '@angular/core';
import { BatteryLevelService } from '../battery-level.service';
import { BluetoothCore } from '@manekinekko/angular-web-bluetooth';
@Component({
  selector: 'ble-battery-level',
  //...
  providers: [ BatteryLevelService, BluetoothCore ]
})
export class BatteryLevelComponent implements OnInit { //...

WebBluetoothModule.forRoot() は、内部で navigator.bluetooth を使用する BrowserWebBluetooth 実装も提供します。Angular Universal 用の ServerWebBluetooth 実装は後で提供される予定です。もちろん、Angular の DI を使用して、カスタム実装を提供することも可能です。

BatteryLevelService (battery-level.service.ts) サービスは、デバイスやセンサーのロジックを実装する場所です。以下の例では、接続されたデバイスのバッテリーレベルを読み取るバッテリーレベルサービスを実装しています:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
// import BluetoothCore and types 
import {
  BluetoothCore,
  BluetoothRemoteGATTServer,
  BluetoothRemoteGATTService,
  BluetoothRemoteGATTCharacteristic,
  DataView
} from '@manekinekko/angular-web-bluetooth';
@Injectable()
export class BatteryLevelService {
  // define your services and characteristics
  static GATT_CHARACTERISTIC_BATTERY_LEVEL = 'battery_level';
  static GATT_PRIMARY_SERVICE = 'battery_service';
  constructor(
    // inject the BluetoothCore
    public ble: BluetoothCore
  ) {}
  getDevice() {
    // you can get ask for the device observable in order to be notified when the device has (dis)connected
    return this.ble.getDevice$();
  }
  streamValues() {
    // for realtime values, you can call this method and subscribe in order to receive the stream of realtime values
    return this.ble.streamValues$()
      .map( (value: DataView) => value.getUint8(0));
  }
  getBatteryLevel(): Observable<number> {
    return this.ble 
        // 1) call this method to run the discovery process
        .discover$({
          optionalServices:[
            BatteryLevelService.GATT_PRIMARY_SERVICE
          ]
        })
        // 2) you'll get the GATT server
        .mergeMap( (gatt: BluetoothRemoteGATTServer)  => {
          // 3) get the primary service of that GATT server
          return this.ble.getPrimaryService$(
            gatt, 
            BatteryLevelService.GATT_PRIMARY_SERVICE
          );
        })
        .mergeMap( (primaryService: BluetoothRemoteGATTService) => { 
          // 4) get a specific characteristic 
          return this.ble.getCharacteristic$(
            primaryService,
            BatteryLevelService
               .GATT_CHARACTERISTIC_BATTERY_LEVEL
          ); 
        })
        .mergeMap( 
          (characteristic: BluetoothRemoteGATTCharacteristic) =>  {
            // 5) read the provided value (as DataView)
            return this.ble.readValue$(characteristic);
          }
        )
        // 6) get the right value from the DataView
        .map( (value: DataView) => value.getUint8(0) );
  }
}

説明します

では、getBatteryLevel() メソッド内で何が起こっているのか説明しましょう…

基本的に、デバイスから値を読み取るには、次のような流れを経る必要があります(一般的な使用ケースの場合):

  1. discover$() メソッドを呼び出して、発見プロセスを実行します。

  2. これにより、GATT サーバーが返されます。

  3. 次に、その GATT サーバーの主要なサービスを取得します。

  4. 次に、特定の特徴を取得します。

  5. 最後に、その特徴から抽出した値を読み取ります(DataView として)。

  6. 最後のステップでは、値が DataView タイプとして取得されます。デバイスやセンサーに特有の適切な値を読み取る必要があります。例えば、単純な値の場合(例えば バッテリーレベル)、value.getUint8(0) を呼び出すだけで十分です:

.map( (value: DataView) => value.getUint8(0) );

ただし、場合によっては、状況がもっと複雑になることがあります。一部のメーカーは、Bluetooth GATT 特徴を標準に従わずに独自に実装することがあります。例えば、Luxometer(一般的には光センサーと呼ばれる、LUX で測定するもの)の場合がこれに該当します。以下は Texas Instrument SensorTag CC2650 センサーに関連するサンプルコードです:

.map( (data: DataView) => { let value = data.getUint16(0, true /* little endian */); let mantissa = value & 0x0FFF; let exponent = value » 12; let magnitude = Math.pow(2, exponent); let output = (mantissa * magnitude); let lux = output / 100.0; return +lux.toFixed(2); });

これは通常、デバイスやセンサーのドキュメントやソースコードで見つけることができます、運が良ければ!