cordova-plugin-bluetoothle を使って iOS 同士で Bluetooth 通信する Cordova アプリを作る : 5 セントラル編 (後編)

前回の続き。

前回までで、セントラル端末として動作するのに必要な API を Promise 化して用意するところまでができた。

今回は各 API にオプションを渡し、実際に動作させるためのコンポーネント側の実装を進めていく。

まずは処理を並べてみる

ペリフェラル側のときと同じように、まずは処理を並べてみる。

@Component({ /* 省略 */ })
export class CentralComponent {
  // プロパティの類、およびサービスクラスの DI は省略
  
  /** 「セントラル通信開始」ボタン押下時の処理 */
  execCentral() {
    // メッセージ表示 (updateMessage() の実装は PeripheralComponent と同じなので省略)
    this.updateMessage('セントラル通信開始');
    
    // ペリフェラル端末のアドレスを控えておく退避変数
    let address;
    
    // セントラルの初期化処理
    this.centralService.initialize()
      .then(() => {
        this.updateMessage('初期化完了・スキャン開始');
        // 定数から探索対象のアドバタイジング名を指定する
        return this.centralService.startScan(bluetoothConstants.advertisingName);
      })
      .then((targetAddress) => {
        this.updateMessage('スキャン完了・接続開始');
        // スキャンしたペリフェラル端末のアドレスを退避変数に控えておく
        address = targetAddress;
        
        // オプションに address を指定して接続する
        return this.centralService.connect({
          address: address
        });
      })
      .then(() => {
        this.updateMessage('接続完了・サービス情報取得開始');
        return this.centralService.discover({
          address: address
        });
      })
      .then(() => {
        this.updateMessage('サービス情報取得完了・write 要求送信開始');
        return this.centralService.write( /* TODO : オプション */ );
      })
      .then(() => {
        this.updateMessage('write 要求送信完了・read 要求送信開始');
        return this.centralService.read( /* TODO : オプション */ );
      })
      .then((readResult) => {
        // value 値があれば read 要求に対する応答の受信に成功
        if(readResult.value) {
          this.updateMessage('read 要求送信完了・応答メッセージを出力');
          // メッセージをデコードしてテキストボックスに出力する
          this.cReceiveText = this.bluetoothService.decodeText(readResult.value);
        }
        
        this.updateMessage('切断処理開始');
        return this.centralService.disconnect({
          address: address
        });
      })
      .then(() => {
        this.updateMessage('切断処理完了・通信終了処理開始');
        return this.centralService.close({
          address: address
        });
      })
      .then(() => {
        this.updateMessage('通信終了処理完了・セントラル通信の終了処理が完了');
      })
      .catch((error) => {
        // どこかの処理で失敗したらエラーメッセージを表示
        this.updateMessage(`ペリフェラル通信終了処理に失敗しました : ${error}`);
      });
  }
}

startScan にはアドバタイジング名を渡す。connectdiscoverdisconnectclose はいずれも address プロパティに接続対象のアドレスを指定するだけなので実装してしまった。

スキャン後の接続開始時は、connect を呼び、そのあとに discover を呼んでからでないと、writeread ができない。writeread の詳細はこのあと説明する。

通信の切断処理も、disconnect してから close を呼ばないと、完全に通信を切ることができない。ココはお決まりのパターンなので固定で覚えてしまう。

writeread のオプション設定

それでは、writeread のオプション設定にうつる。

// execCentral() 内

// 省略
.then(() => {
  this.updateMessage('サービス情報取得完了・write 要求送信開始');
  return this.centralService.write({
    // 退避変数のアドレスを指定する
    address: address,
    // 定数からサービス・キャラクタリスティックを指定する
    service: bluetoothConstants.serviceUuid,
    characteristic: bluetoothConstants.characteristicUuid,
    // テキストボックスの文字列をエンコードして送信する
    value: this.bluetoothService.encodeText(this.cSendText),
    // ペリフェラル端末からの応答を待たずに write を成功させる
    type: 'noResponse'
  });
})
.then(() => {
  this.updateMessage('write 要求送信完了・read 要求送信開始');
  return this.centralService.read({
    // 退避変数のアドレスを指定する
    address: address,
    // 定数からサービス・キャラクタリスティックを指定する
    service: bluetoothConstants.serviceUuid,
    characteristic: bluetoothConstants.characteristicUuid
  });
})
.then((readResult) => {
  // value 値があれば read 要求に対する応答の受信に成功
  if(readResult.value) {
    this.updateMessage('read 要求送信完了・応答メッセージを出力');
    // メッセージをデコードしてテキストボックスに出力する
    this.cReceiveText = this.bluetoothService.decodeText(readResult.value);
  }
// 以後略

ココで定数の情報を使い、サービスおよびキャラクタリスティックを指定する。この情報自体は discover の結果から取得することも可能ではあるが、どうせ1つのサービス・キャラクタリスティックしか提供していないので、定数から直に指定すれば良い。

writevalue プロパティに、エンコードした文字列を渡す。これが、ペリフェラル側では writeRequested で受け取れる文字列となるワケだ。type: 'noResponse' を指定することで、write 要求に対するペリフェラル側の respond() を待たなくなる。ペリフェラル側で指定した writeWithoutResponse: true と合わせて指定しておくことで確実になる。

read は、ペリフェラル側に「何か送り返して〜」と読み取りを要求するだけなので、指定するのはアドレス・サービス・キャラクタリスティックのみ。この read 要求に対し、ペリフェラル側が readRequestedrespond 処理を行うと、セントラル側の read のコールバック関数が実行される、という動きになる。結果オブジェクト readResultvalue プロパティが respond された値になっているので、これをデコードして readonly なテキストボックスに表示させてやれば OK。

writereadrespond に関しては、どうも連続してやり取りしようとすると上手くいかなくなるところがあった。

例えば、connect してから一度でも read すると、その後 write 要求に respond しても、応答メッセージが直前の read に対する respond で送信した値しか渡らなくなってしまうのだ。respond したペリフェラル側では正しく新たなメッセージを応答しているのだが、なぜかセントラル側では最初の read で受け取ったメッセージを write でも受け取ってしまうのだ。

connect してから disconnect するまでの間で、write の前に read をしない通信パターンなら問題なく write に対する respond でメッセージが返せたが、どうも write に対する respond が絡むと意図したとおりに動作しないことがあるので、一度 readwrite をしたら disconnect して、都度再接続するような作りにした方が安全そうだった。


5回に分けて説明してきた cordova-plugin-bluetoothle プラグインだったが、これでペリフェラル・セントラルの両方の実装ができた。あとは2台の実機にこの Cordova アプリをインストールし、片方はペリフェラル画面、もう片方はセントラル画面を開き、同時に「通信開始」ボタンを押せば良い。今回の実装でいえば、通信の流れは以下のようになる。

  1. ペリフェラル:初期化 → サービス追加 → アドバタイジング開始
  2. セントラル:初期化 → スキャン開始 → (アドバタイジングが開始したら取得できるようになるので) ペリフェラル端末検知 → スキャン停止
  3. セントラル:write 要求送信 (画面入力されたテキストを送信する)
  4. ペリフェラル:writeRequested 発火 → 受信したメッセージを画面に表示する
  5. セントラル:read 要求送信
  6. ペリフェラル:readRequested 発火 → 画面入力されたテキストを返送する → ペリフェラル終了処理
  7. セントラル:read 要求の結果受信 → 返信されたメッセージを画面に表示する → 切断 → セントラル終了処理

今回紹介しなかったが、セントラル側が subscribe() で受信待機し、ペリフェラル側が notify() で任意のタイミングでメッセージを発信する、という API もある。こちらは unsubscribe() するまでずっと受信待機できるし、ペリフェラル側はセントラルとの通信状況に関係なく notify() で情報発信ができて面白い。

また、今回の実装は「ペリフェラル」画面と「セントラル」画面を分けて実装したが、トグルボタンなどで「ペリフェラルモード」と「セントラルモード」を切り替えて1画面で実装しても良いだろう。

「ペリフェラル」「セントラル」といった役割を意識せずに、自動的にどちらかがペリフェラル、どちらかがセントラルになるような実装ができたら、よりユーザにとって分かりやすい画面になりそう。どうやったらいいんだろうな、はじめはセントラル端末として周辺端末をスキャンしつつ、いなさそうなら initializePeripheral しちゃえばいいのかしら?

色々と遊べそうな cordova-plugin-bluetoothle でした。本編はココで終わりだが、次回は開発時のモック化の方法を紹介する。