Corredor

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

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

HTML5とApache Cordovaで始めるハイブリッドアプリ開発 (CodeZine BOOKS)

HTML5とApache Cordovaで始めるハイブリッドアプリ開発 (CodeZine BOOKS)

前回の続き。

前回までで、セントラル端末として動作するのに必要な 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 でした。本編はココで終わりだが、次回は開発時のモック化の方法を紹介する。

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

Bluetooth LE入門スマホにつながる低消費電力無線センサの開発をはじめよう

Bluetooth LE入門スマホにつながる低消費電力無線センサの開発をはじめよう

前回の続き。

前回までで、ペリフェラル側の実装が完了した。今回はセントラル側の実装に移る。

セントラル端末側は、アドバタイジング名から通信対象のペリフェラル端末を特定して「アドレス」を取得する。そのアドレスで対象のペリフェラル機器と接続し、サービス・キャラクタリスティック名を指定して要求を送信するワケである。

セントラル側の画面の実装

まずはペリフェラル側と同様に、セントラル側の画面となるコンポーネントを実装する。

<h1>セントラル</h1>

<dl>
  <dt>ペリフェラルに送信するテキスト</dt>
  <dd>
    <p><input type-"text" name="c-send-text" [(ngModel)]="cSendText"></p>
  </dd>
  <dt>ペリフェラルから受信した応答テキスト (読取専用)</dt>
  <dd>
    <p><input type="text" name="c-received-text" [value]="cReceivedText" readonly></p>
  </dd>
</dl>

<p>
  <input type="button" (click)="execCentral()" value="セントラル通信開始">
</p>

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

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

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

セントラル通信に必要な API の Promise 化

続いて cordova-plugin-bluetoothle プラグインの API のうち、セントラル側で使用する API を Promise 化したサービスをこしらえる。

今回使用する API は以下のとおり。

  • initialize … 第2引数 failureCallback、第3引数 options なし
  • startScan … 後述
  • isScanning … startScan と合わせて実装・後述
  • stopScan … startScan と合わせて実装・後述
  • connect
  • discover
  • write
  • read
  • disconnect
  • close

initialize は第2引数の errorCallback なし。startScanisScanningstopScan は組み合わせで実装するので後述。それ以外は successCallbackfailureCallbackoptions の順で引数を取るので、以下のように実装する。

@Injectable()
export class CentralService {
  /** セントラル端末の初期化処理 */
  initialize(): Promise<any> {
    return new Promise((resolve) => {
      (window as any).bluetoothle.respond(
        // successCallback のみ
        (result) => { resolve(result); }
      );
    });
  }
  
  /** スキャン開始処理 */
  startScan(): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.startScan(
        (result) => {
          // TODO : 後で isScanning・stopScan との組み合わせで実装する
          resolve(result);
        },
        (error) => { reject(error); }
      );
    });
  }
  
  /** 指定のアドレスのペリフェラル端末と接続する */
  connect(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.connect(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
  
  /** 指定のアドレスのペリフェラル端末の情報を取得する */
  discover(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.discover(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
  
  /** 指定のペリフェラル端末に write 要求を送信する */
  write(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.write(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
  
  /** 指定のペリフェラル端末に read 要求を送信する */
  read(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.read(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
  
  /** 指定のアドレスのペリフェラル端末との接続を切断する */
  disconnect(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.disconnect(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
  
  /** 指定のアドレスのペリフェラル端末との通信を終了する */
  close(options: Object): Promise<any> {
    return new Promise((resolve, reject) => {
      (window as any).bluetoothle.close(
        (result) => { resolve(result); },
        (error) => { reject(error); },
        options
      );
    });
  }
}

スキャンの開始と終了を自動化する

上述のサービスでは startScan() というラッパーメソッドを作ったが、startScan はペリフェラル端末のスキャンを開始するだけで、自動的にスキャンを停止したりしてくれない。そこで、目的のペリフェラル端末を見つけたり、指定秒数以内に見つからなかったりした時にスキャンを停止する処理を盛り込もうと思う。

/** スキャン開始処理 : 引数で指定したアドバタイジング名の端末のアドレスを返却する */
startScan(advertisingName: string): Promise<any> {
  return new Promise((resolve, reject) => {
    // 探索したペリフェラル端末のアドレスを控えておく退避変数
    let address;
    
    // スキャン停止処理を用意する
    const stopScan = () => {
      (window as any).bluetoothle.isScanning((result) => {
        // スキャン中なら停止処理を呼ぶ
        if(result.isScanning) {
          (window as any).bluetoothle.stopScan((scanResult) => {
            // アドレスが取得できていればアドレスを Resolve する
            if(address) {
              resolve(address);
            }
            else {
              reject('探索失敗');
            }
          }, (error) => {
            reject(error);
          });
        }
        else {
          // もしスキャンしていない場合も、アドレスが取得できていればアドレスを Resolve する
          if(address) {
            resolve(address);
          }
          else {
            reject('探索失敗');
          }
        }
      });
    };
    
    // 10秒後にスキャンを停止するタイマーをセットする
    const stopScanTimer = setTimeout(() => {
      stopScan();
    }, 10 * 1000);
    
    // スキャンを開始する
    (window as any).bluetoothle.startScan((result) => {
      // stopScan() するまでこのコールバック関数が繰り返し呼ばれる
      
      // 指定のアドバタイジング名を探索する
      if(result.advertisement && result.advertisement.localName === advertisingName) {
        // 退避変数にアドレスを控えておく
        address = result.address;
        
        // タイマーを解除した上でスキャンを停止する
        clearTimeout(stopScanTimer);
        stopScan();
      }
    }, (error) => {
      // スキャン開始に失敗した場合はスキャン停止タイマーを解除して終了する
      clearTimeout(stopScanTimer);
      reject(error);
    });
  });
}

一旦コードの前半は飛ばして、startScan の中身。アドバタイジング名は advertisement.localName で確認できるので、これが引数で指定した advertisingName と一致していたら、stopScan() 処理を呼んで終了している。

横着して stopScan() のネストが深めになっているが、スキャン中ならスキャンを停止するようにしている。Promise を resolve() するのはこの stopScan() の中で、退避変数 address の値を resolve() するようにしている。これにより、呼び出し側にペリフェラル端末のアドレスが返却されるので、以降でアドレスを指定した処理が呼び出せるというワケ。

startScan してから一定時間以上経ったら stopScan() を呼ぶようにするため、setTimeout を使ったタイマーを定義している。

  • ずっと探索中が続いたら、このタイマーにより stopScan() が実行されて、恐らく reject() される。
  • 指定のペリフェラル端末が見つかったら、clearTimeout() でタイマーを解除し、その上で stopScan() が呼ばれ、ほぼ確実に resolve() となる。
  • もし startScan 自体に失敗した場合はエラーコールバックの方でタイマーを解除し、その場で reject() する。この場合は stopScan() は実行されない。

タイマーを使った実装は、cordova-plugin-bluetoothle プラグインの作者が AngularJS 向けに作成した ngCordova ラッパーの実装を参考にした。


これでセントラル端末の通信に使う API が用意できた。次回はこれをコンポーネント側から呼び出していく。

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

iOS×BLE Core Bluetoothプログラミング

iOS×BLE Core Bluetoothプログラミング

前回の続き。

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

今回は各 API にどのようなオプションを渡して動かせば良いのか紹介し、initializePeripheral の特殊な動きを確認していきたい。

まずは処理を並べてみる

まずはコンポーネント側の「ペリフェラル通信開始」ボタンを押下したときの処理として、前回作ったサービスクラスのメソッドを並べるだけ並べてみようと思う。ペリフェラル端末はこのように通信準備を行い、通信を止めていく、ということを確認しよう。

@Component({ /* 省略 */ })
export class PeripheralComponent {
  // プロパティの類、およびサービスクラスの DI は省略
  
  /** 「ペリフェラル通信開始」ボタン押下時の処理 */
  execPeripheral() {
    // メッセージ更新 (基本的には console.log と同等と思ってください)
    this.updateMessage('ペリフェラル通信開始');
    
    // 一定時間セントラル端末からの要求がなければ終了処理を実行するためのタイマー
    let waitTimer;
    
    this.peripheralService.initializePeripheral((result) => {
      // TODO : initializePeripheral のコールバック関数は後で実装
    })
      .then(() => {
        this.updateMessage('ペリフェラル初期化完了・サービス追加開始');
        return this.peripheralService.addService( /* TODO : オプション */ );
      })
      .then(() => {
        this.updateMessage('サービス追加完了・アドバタイジング開始');
        return this.peripheralService.startAdvertising( /* TODO : オプション */ );
      })
      .then(() => {
        this.updateMessage('アドバタイジング開始・セントラル端末の通信待機中…');
        
        // 10秒間セントラル端末からの応答がなければ終了処理を呼び出すためタイマーを設置する
        // TODO : タイマーの解除処理は別途実装する
        waitTimer - setTimeout(() => {
          this.updateMessage('10秒間応答がなかったため中断します');
          this.destroyPeripheral();
        }, 10 * 1000);
      })
      .catch((error) => {
        // どこかの処理で失敗したらエラーメッセージを表示
        this.updateMessage(`ペリフェラル通信開始処理に失敗しました : ${error}`);
      });
  }
  
  /** 進捗を示すメッセージを更新し描画を強制更新する */
  private updateMessage(message: string) {
    this.message = message;
    this.changeDetectorRef.detectChanges();
  }
  
  /** ペリフェラル端末の終了処理 */
  private destroyPeripheral() {
    this.updateMessage('終了処理開始 : アドバタイジング終了');
    this.peripheralService.stopAdvertising()
      .then(() => {
        this.updateMessage('アドバタイジング終了完了・サービス終了開始');
        return this.peripheralService.removeAllServices();
      })
      .then(() => {
        // 全て正常終了
        this.updateMessage('サービス終了完了・ペリフェラル通信の終了処理が完了');
      })
      .catch((error) => {
        // どこかの処理で失敗したらエラーメッセージを表示
        this.updateMessage(`ペリフェラル通信終了処理に失敗しました : ${error}`);
      });
  }

今回のサンプルは、複雑な要求・応答のやりとりの中でのエラーハンドリングをしないようにするため、「接続して通信したら切断する」という一連の動作をほぼ自動で行うようにしている。

サービスクラスに用意したメソッドのうち、respond だけ登場していない。また、waitTimer というタイマー変数のキャンセル処理もない。これはこのあと initializePeripheral のコールバックの中で実装する。

updateMessage() という関数では、this.message を更新しながら、this.changeDetectorRef.detectChanges() を呼んでいる。これは何かというと、Angular の ChangeDetectorRef クラスのことで、画面を強制的に再描画させるための API だ。前回も触れたように、initializePeripheral のコールバックが発火するタイミングが通常のイベントの範囲とは異なる特殊なタイミングになるので、Angular がイベントとして検知できない瞬間があるのだ。そうすると this.message の値を変えても画面が再描画されず、メッセージが更新されたことが分からないのである。これを回避するために detectChanges() を呼んでいるワケだ。以前以下の記事でも紹介しているので、こちらも参照してほしい。

neos21.hatenablog.com

initializePeripheral のコールバック関数を実装する

いよいよ initializePeripheral のコールバック関数を実装する。この実装を見てやっと、「特殊なタイミングで発火する」の意味が分かっていただけるかと思う。

// execPeripheral() 内

// 一定時間セントラル端末からの要求がなければ終了処理を実行するためのタイマー
let waitTimer;

this.peripheralService.initializePeripheral((result) => {
  // ステータスからコールバックの発火内容を分ける
  
  if(result.status === 'writeRequested') {
    this.updateMessage('write 要求を受信しました');
    
    // 受信したテキスト (result.value) をデコードしてテキストボックスに出力する
    // TODO : BluetoothService#decodeText() は後で実装を紹介する
    this.pReceiveText = this.bluetoothService.decodeText(result.value);
  }
  else if(result.status === 'readRequested') {
    this.updateMessage('read 要求を受信しました');
    
    // 処理中断用のタイマーを解除する
    if(waitTimer) {
      clearTimeout(waitTimer);
    }
    
    // result.requestId で特定できる read 要求に対して、応答してから終了する
    // TODO : BluetoothService#encodeText() は後で実装を紹介する
    this.peripheralService.respond({
      requestId: result.requestId,
      value: this.bluetoothService.encodeText(pReceiveText)
    })
      .then(() => {
        // 応答に成功したら終了処理を呼ぶ
        this.destroyPeripheral();
      })
      .catch(() => {
        // 応答に失敗した場合も終了処理を呼ぶ
        this.destroyPeripheral();
      });
  }
  else {
    // その他の場合は今回は特に何もしない
    this.updateMessage(`次のイベントが発生しました : ${result.status}`);
  }
  
  // いずれの処理の場合も画面を強制的に再描画するためココでも ChangeDetectorRef を呼んでおく
  this.changeDetectorRef.detectChanges();
})
  .then(() => {
    this.updateMessage('ペリフェラル初期化完了・サービス追加開始');
    /* 以下略 */
    });

initializePeripheral のコールバック関数には引数が1つ渡されており (ココでいう result)、この中の status プロパティが、コールバックの実行理由を示している。

取りうる status プロパティは以下で確認できる。

  • status => enabled = Bluetooth is enabled
  • status => disabled = Bluetooth is disabled
  • status => readRequested = Respond to a read request with respond(). Characteristic (Android/iOS) or Descriptor (Android)
  • status => writeRequested = Respond to a write request with respond(). Characteristic (Android/iOS) or Descriptor (Android)
  • status => subscribed = Subscription started request, use notify() to send new data
  • status => unsubscribed = Subscription ended request, stop sending data
  • status => notificationReady = Resume sending subscription updates (iOS)
  • status => notificationSent = Notification has been sent (Android)
  • status => connected = A device has connected
  • status => disconnected = A device has disconnected
  • status => mtuChanged = MTU has changed for device

細かく状況を確認しようと思えば、セントラル端末が接続してきたときに connected ステータスのコールバックが実行されるし、今回はサンプルに含んでいないがセントラル側で subscribe() を使った場合は subscribed ステータスのコールバックが発生する。今回の例では、このうち readRequestedwriteRequested のイベントを検知している。その他のプロパティはステータスに応じて requestId が渡されたり色々と変化する。

後で実装するが、セントラル側では

  1. 先に write 要求によってペリフェラル端末に向けてメッセージを送信し、
  2. 次に read 要求によってペリフェラル端末にメッセージを応答するよう要求する

という処理をするつもりである。そこで、ペリフェラル端末側の実装としては

  1. write 要求を受け取ったら (= writeRequested を検知したら) 受け取ったメッセージをテキストボックスに出力し、
  2. read 要求を受け取ったら (= readRequested を検知したら) テキストボックスの文字列をセントラル端末に返送する

という処理を用意しておく。

それ以外のイベントココでは検知せず素通りさせるが、ChangeDetectorRef#detectChanges() は実行しておくと良い。特に writeRequested の際に受信テキストを表示するところが正しく検知されないので、ココのために実行しておく。

readRequested を受け取ったら、テキストを返送してから、ペリフェラル端末の終了処理 peripheralDestroy() を呼び出しておく。

updateMessage() による進捗メッセージ表示は、実際は高速で連続して更新されるので、通信が始まったら一瞬で送受信 → 終了処理、と流れると思われる。

テキストのエンコード・デコード

BluetoothService#encodeText() と、BluetoothService#decodeText() というメソッドが突如登場したと思う。これは送信する文字列を Base64 文字列にエンコードし、受信したテキストはデコードするための処理。

BluetoothService というサービスクラスを作り、以下のように実装する。

@Injectable()
export class BluetoothService {
  /** 引数の文字列を Base64 エンコードする */
  encodeText(str: string): string {
    const encodedString = btoa(this.windowRefService.nativeWindow.unescape(encodeURIComponent(str)));
    return encodedString;
  }
  
  /** 引数の Base64 文字列をデコードする */
  decodeText(encodedString: string): string {
    const str = decodeURIComponent(this.windowRefService.nativeWindow.escape(atob(encodedString)));
    return str;
  }
}

cordova-plugin-bluetoothle はテキストのエンコード・デコード用メソッドを提供しているのだが、日本語に対応していないため、以前紹介した Base64 エンコードの処理を使い回すことにした。

addServicestartAdvertising のオプション設定

ここまでで、ペリフェラルの初期設定およびイベント検知 (initializePeripheral)、通信終了処理の実装が終わった。

残りは通信開始時のオプション設定だ。これは API リファレンスを参考に、以下のように実装する。前回作成した定数クラス bluetoothConstants を使う。

// 省略
.then(() => {
  this.updateMessage('ペリフェラル初期化完了・サービス追加開始');
  return this.peripheralService.addService({
    // 追加するサービスの UUID を指定する
    service: bluetoothConstants.serviceUuid,
    // サービスに紐付くキャラクタリスティック : 配列で複数指定できるが今回は1つのみ
    characteristics: [{
      // キャラクタリスティック UUID
      uuid: bluetoothConstants.characteristicUuid,
      // 許可設定
      permissions: {
        read: true,
        write: true
      },
      // 通信の設定
      properties: {
        read: true,
        writeWithoutResponse: true,  // レスポンスなしで write() させる設定。true の場合は write に対し respond() できなくなる
        write: true,
        notify: true,
        indicate: true
      }
    }]
  });
})
.then(() => {
  this.updateMessage('サービス追加完了・アドバタイジング開始');
  // アドバタイジングを開始するサービスを指定する
  return this.peripheralService.startAdvertising({
    services: [bluetoothConstants.serviceUuid],  // iOS 向けの書き方 : 配列
    service: bluetoothConstants.serviceUuid,  // Android 向けの書き方もついでに
    // アドバタイジング名
    name: bluetoothConstants.advertisingName
  });
})
// 以後略

このようになる。


これでペリフェラル側の実装が完了した。

次回はセントラル側の実装を行い、今回用意したペリフェラル端末とやり取りできるようにしていこうと思う。