Corredor

ウェブ、プログラミングの勉強メモ。

cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 2 ペリフェラル編 (前編)

Bluetooth Low Energyをはじめよう (Make:PROJECTS)

Bluetooth Low Energyをはじめよう (Make:PROJECTS)

前回の続き。

neos21.hatenablog.com

紹介したとおり、Bluetooth 通信はサーバの役割を担う「ペリフェラル」と、クライアントとしてサーバに接続しにいく「セントラル」という役割に別れて行う。今回はこのサーバ側となる「ペリフェラル」の実装を行っていく。

フロントエンドは Angular4 + TypeScript で実装するが、Corodva プラグインの呼び出し方さえ各フレームワークのやり方に合わせてやれば問題ないだろう。

ペリフェラル側の画面の実装

Angular CLI などを使って、ペリフェラル端末の動作画面となるコンポーネントを作る。画面構成は以下のようにしよう。

<h1>ペリフェラル</h1>

<dl>
  <dt>セントラルから受信したテキスト (読取専用)</dt>
  <dd>
    <p><input type="text" name="p-received-text" [value]="pReceivedText" readonly></p>
  </dd>
  <dt>セントラルに返信するテキスト</dt>
  <dd>
    <p><input type-"text" name="p-response-text" [(ngModel)]="pResponseText"></p>
  </dd>
</dl>

<p>
  <input type="button" (click)="execPeripheral()" value="ペリフェラル通信開始">
</p>

<!-- 動作の進捗を示すメッセージ表示欄 -->
<p>{{ message }}</p>

コンポーネントの実装は以下のような感じ。

@Component({
  selector: 'app-peripheral',
  templateUrl: './peripheral.component.html',
  styleUrls: ['./peripheral.component.scss']
})
export class PeripheralComponent {
  /** セントラル端末から受信したテキスト */
  pReceivedText: string = '';
  
  /** セントラルに返信するテキスト : デフォルト値を設定しておく */
  pResponseText: string = 'ペリフェラルから送信';
  
  /** 動作の進捗を示すメッセージ表示欄 : デフォルト値を設定しておく */
  message: string = '「ペリフェラル通信開始」ボタンを押してください';
  
  /** 「ペリフェラル通信開始」ボタン押下時の処理 */
  execPeripheral() {
    // TODO : これから実装していく
  }

Angular のデータバインディングを使って、セントラルからテキストを受け取ったら pReceivedText プロパティにセットし、画面に表示する、という作り。セントラルに応答するメッセージは pResponseText プロパティで受け取っておき、画面上のテキストボックスで自由に文言を変えられるようにする。ここらへんはフレームワークによって異なるだろうが、「受信したテキストを任意の場所に表示」「テキストボックスの入力値を送信」がやりたいだけなので、各自実装はおまかせ。

message プロパティには、「セントラル端末と通信を開始しました」みたいなメッセージを逐一表示しようと思う。別に必須ではないが、動いている感が分からないので入れておく。

execPeripheral() メソッド内にペリフェラル端末として通信するための処理を書いていくのだが、ココに cordova-plugin-bluetoothle プラグインの API をゴリゴリ書いていくと長くなる上にコールバック地獄に陥りやすいので、API を Promise 化したサービスクラスを作ろうと思う。

だが、サービスクラスの実装をする前に、少し準備。

ペリフェラル端末の情報を決めておく

cordova-plugin-bluetoothle プラグインを使ってペリフェラル端末を構築するために、最低限以下の情報を決めておく必要がある。

  • アドバタイジング名
  • サービス UUID (Universally Unique IDentifier)
  • キャラクタリスティック UUID

「アドバタイジング」とは、これはペリフェラル端末が周辺に自分の存在を知らせるために発信している信号のこと。通信対象の端末を特定するために、アドバタイジングで発信する文言を作っておこう。

次に、サービスとキャラクタリスティック。ペリフェラル端末は、複数の「サービス」を提供していて、各サービスの中に「キャラクタリスティック」というモノを複数持っている。Bluetooth 通信の際は、この「サービス」と「キャラクタリスティック」を特定して通信する必要がある。

このあたりの概念は今回は理解しきれなくても良いが、以下の文献で用語を押さえておくとスッキリするかもしれない。

セントラル側の目線で見ると、まず周辺のアドバタイジングを雑多に取得していき、予め決めておいた「アドバタイジング名」と一致する端末と接続しようとするワケだ。そして、そのペリフェラル端末の内の、決めておいたサービス UUID およびその配下の決められたキャラクタリスティック UUID を指定して、通信を行うことになる。

これらの情報はセントラル側でも共用するので、任意の ValueObject クラスを作って定義しておこう。

/** Bluetooth 通信で使う定数クラス */
export const bluetoothConstants = {
  /** アドバタイジング名 */
  advertisingName: 'MyExampleBLE',
  /** サービス UUID */
  serviceUuid: 'これから決める',
  /** キャラクタリスティック UUID */
  characteristicUuid: 'これから決める'
};

アドバタイジング名は好きに決めて良い。ココでは 'MyExampleBLE' とした。

サービス UUID は、その名のとおりユニークでないといけない。Mac の場合、uuidgen というコマンドがあり、ターミナルでこのコマンドを打つと、UUID を生成できる。試しにやってみよう。

$ uuidgen
2F1B27A0-C1F8-44FA-930C-134BF9B5AAFF

何か出てきた。これをそのまま使うことにする。そうそう身の回りで被ることもないのでコレを固定で良いだろう。

キャラクタリスティック UUID は4文字の英数字で決める。これユニークである必要があるが、今回は1つのサービスだけ提供して、そのサービスの中でも1つしかキャラクタリスティックを持たないので、適当で良いだろう。今回は 'ABCD' とでもしておく。

というワケで定数クラスはこうなる。

/** Bluetooth 通信で使う定数クラス */
export const bluetoothConstants = {
  /** アドバタイジング名 */
  advertisingName: 'MyExampleBLE',
  /** サービス UUID */
  serviceUuid: '2F1B27A0-C1F8-44FA-930C-134BF9B5AAFF',
  /** キャラクタリスティック UUID */
  characteristicUuid: 'ABCD'
};

これでペリフェラル端末の特定に必要な情報の定義は終わり。

サービスクラスの用意

いよいよ cordova-plugin-bluetoothle プラグインを使った実装に移る。

先程のように Angular CLI を使ったりして、サービスクラスを作る。

@Injectable()
export class PeripheralService {
}

ココに、cordova-plugin-bluetoothle プラグインの各 API を Promise 化したメソッドを作っていく。

なお、cordova-plugin-bluetoothle プラグインはメソッドの引数の順番が大体決まっていて、ほとんど以下のようになっている。

// addService というメソッド
window.bluetoothle.addService(successCallback, failureCallback, options);

第1引数が成功時のコールバック関数、第2引数が失敗時のコールバック関数、第3引数はメソッドによるが、オプションをオブジェクト (連想配列) で渡す。

これを Promise 化すると、以下のようなメソッドになる。

// 第3引数のオプションはサービスメソッドの引数で受け取ることにする
addService(options: Object): Promise<any> {
  return new Promise((resolve, reject) => {
    // 型定義回避のための as any
    (window as any).bluetoothle.addService(
      (result) => { resolve(result); },
      (error) => { reject(error); },
      options
    );
  });
}

今回ペリフェラル端末として利用する API は以下のとおり。

  • initializePeripheral … 後述
  • addService
  • startAdvertising
  • respond
  • stopAdvertising … 第3引数 options なし
  • removeAllServices … 第3引数 options なし

addService は上のコードのとおり。startAdvertisingrespondaddService のメソッド名だけ変える感じ。stopAdvertisingremoveAllServices は第3引数がないので以下のようになる。

stopAdvertising(): Promise<any> {
  return new Promise((resolve, reject) => {
    (window as any).bluetoothle.stopAdvertising(
      (result) => { resolve(result); },
      (error) => { reject(error); }
    );
  });
}

あとは任意で resolve() の直前にコンソールログ出力などの処理を挟んでも良い。

initializePeripheral だが、このメソッドだけは特殊な動きをするので、以下のように実装するのをオススメする。

// 成功時に実行するコールバック関数を引数で受け取る
initializePeripheral(successCallback: Function): Promise<any> {
  return new Promise((resolve, reject) => {
    (window as any).bluetoothle.initializePeripheral(
      (result) => {
        // コールバック関数を実行する
        successCallback(result);
        resolve(result);
      },
      (error) => { reject(error); }
    );
  });
}

引数に受け取った関数 successCallback を実行しつつ resolve() している。なぜこのように実装するかというと、initializePeripheral のコールバック関数は、セントラル端末と通信を開始したり、セントラル端末から要求を受け取ったりなど、Bluetooth 通信の状況が変化する度に繰り返し呼び出されるのだ。つまり、このコールバック関数だけは1回の通信の中で複数回実行されるため、画面描画などの作りを考えて、コールバック関数をサービスに渡してやって動かす方が都合が良いのだ。このあとのコンポーネント実装でその意図をお見せしよう。

ということで、サービスクラスは以下のようになる。

@Injectable()
export class PeripheralService {
  /** ペリフェラルの初期化処理 : 通信状況が変化する度にコールバック関数が再実行される */
  initializePeripheral(successCallback: Function): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.initializePeripheral(
        (result) => {
          // コールバック関数を実行する
          successCallback(result);
          resolve(result);
        },
        (error) => { reject(error); }
      );
    });
  }
  
  /** サービスを追加する */
  addService(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.addService(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
  
  /** アドバタイジングを開始する : セントラル端末が自機を発見できるようになる */
  startAdvertising(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.startAdvertising(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
  
  /** セントラル端末からの要求に応答する */
  respond(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.respond(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
  
  /** アドバタイジングを終了する */
  stopAdvertising(): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.stopAdvertising(
        (result) => { resolve(result); },
        (error) => { reject(error); }
      );
    });
  }
  
  /** サービスを全て削除する */
  removeAllServices(): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.removeAllServices(
        (result) => { resolve(result); },
        (error) => { reject(error); }
      );
    });
  }
}

これで、ペリフェラル端末が1回通信して終了するライフサイクルを網羅する API が用意できた。

重要なのはココから。各メソッドにどのようなオプションを渡せば良いのか、そして initializePeripheral のコールバック関数はどのように動作するのか。

長くなったので後半に続く。

neos21.hatenablog.com neos21.hatenablog.com neos21.hatenablog.com neos21.hatenablog.com