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

前回の続き。

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

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

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

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

<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引数の 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 を使ったタイマーを定義している。

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


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