JavaScript
Chrome
WebBluetooth
microbit

micro:bitのBluetoothとMacのChromeをWeb Bluetooth APIでつなぐ

 2018年1月26日〜28日、電子デバイスGlobal Game Jam 2018を開催しました。僕は参加者としての参加ではなく、主催者側でしたが、手を動かす時間があったので、先日の別のイベントで使った「電池ボックス+スピーカー拡張をしたmicro:bit」を使ったミニゲームを作ることにしました。

 これまで、シリアル通信でPC/Macと通信するプログラムは書いていましたが、今回はmicro:bitに載っているBluetoothを使いMacと接続、ブラウザ上で動くJavaScriptのWeb Bluetooth APIを用いてプログラミングしてみることにします。

参考リンク

 Web Bluetooth APIを用いたmicro:bitとの通信プログラムの実例はまだそれほど多くはありませんが、既に何人かの方が先行していただいているので、それらのページを参考に進めていきます。

先行事例(国内)

 中でも、このリストの一番下のQiitaの@yokmama氏の記事は、今回加速度センサーを使いたかったこともあり、とても参考にさせてもらいました。

先行事例(海外)

  • ランカスター大学「micro:bit runtime BBC micro:bit Bluetooth Profile」 hhttps://lancaster-university.github.io/microbit-docs/ble/profile/
  • micro:bit(公式) reference 「Bluetooth」 https://makecode.microbit.org/reference/bluetooth

 SERVICEやCHARACTERISTICSのUUIDを調べるのにも、これらの公式サイトはとても重要です。

Web Bluetooth API

 Web Bluetooth Community Groupのドキュメントも非常に参考になります。サンプルコードなどもあり困ったときはこちらを見るといいかもしれません。
- Web Bluetooth Community Group「Web Bluetooth Draft Community Group Report, 18 August 2017」https://webbluetoothcg.github.io/web-bluetooth/
- 非公式日本語版「Web Bluetooth Draft Community Group Report, 2016/07/27」※公式に比べて古い https://tkybpp.github.io/web-bluetooth-jp/

事前準備

今回は、MacのChromeで動作確認をしています。(※注:WindowsのChromeでは動作しませんでした)

  • MacBook Pro (Retina, 13-inch, Late 2013)
    • macOS High Sierra 10.13.2
  • Chrome 63.0.3239.132(64bit)

micro:bit側のファームウェア

このページから、firmwareのhexファイルをダウンロードしてもいいのですが、今回は先程の事例にもあった通り、micro:bitのJavaScriptブロックエディタで必要なサービスのみ開始するファームウェアを用意することにしました。

ブロックエディタは、場合によってはBluetoothのパッケージではなく、無線(Radio)のパッケージが追加されている場合があります。その場合は「高度なブロック」から「パッケージを追加する」を選択し、その後の画面でBluetoothを選択すると、無線(Radio)の代わりにBluetoothが使えるようになります。

ブロックは以下のように並べます。「最初だけ」のブロックに、必要となるBluetoothサービスを追加します。接続状態がわかるようにアイコンの表示のブロックも並べてあります。
JavaScriptブロックエディタでの実際の画面

また、右上の歯車のアイコンで「プロジェクトの設定」を選択、設定画面で一番上の「No Pairing Required: Anyone can connect via Bluetooth.」を選びます。
image.png

できあがったhexファイルをmicro:bitに書き込みして、準備完了です。

Bluetooth Low Energy接続のための情報を集める

micro:bitとは、BLEでの接続となります。BLEの接続の手順は大まかに以下のようになっています。

  1. DEVICEの検索(このとき、特定の名前のパターンや、必要とするSERVICEをもっているかなどのフィルタリングをかけて検索します)
  2. DEVICEのGATTサーバへの接続
  3. GATTサーバからSERVICEの取得
  4. SERVICEからCHARACTERISTICSの取得
  5. CHARACTERISTICSから値を取得したり、CHARACTERISTICSに値を書き込んだりする

この手順のために、利用したいSERVICEとCHARACTERISTICSのUUIDが必要となります。今回は加速度センサーとボタンを利用したかったので、必要となるUUIDは以下の通りとなります。

SERVICE / CHARACTERISTICS UUID
ACCELEROMETER SERVICE E95D0753251D470AA062FA1922DFA9A8
ACCELEROMETER DATA CHARACTERISTICS E95DCA4B251D470AA062FA1922DFA9A8
ACCELEROMETER PERIOD CHARACTERISTICS E95DFB24251D470AA062FA1922DFA9A8
BUTTON SERVICE E95D9882251D470AA062FA1922DFA9A8
BUTTON A CHARACTERISTICS E95DDA90251D470AA062FA1922DFA9A8
BUTTON B CHARACTERISTICS E95DDA91251D470AA062FA1922DFA9A8

今回、複数のSERVICEと、それぞれのSERVICEでも複数のCHARACTERISTICSを使う形となります。

JavaScript側のコード

JavaScript側のコードの全体はgithubにアップしています。
https://github.com/hine/microbitble

簡単に試したい場合は、

git clone https://github.com/hine/microbitble.git

するか、zipでダウンロードして、index.htmlをダブルクリックしてChrome等で開いてください。

image.png

このような画面が開き、「CONNECT」ボタンをクリックするとmicro:bit検索のウィンドウが開き、つなぎたいmicro:bitを選択するとその後接続され、Accel_x、Accel_y、Accel_zの値が変化、micro:bitボタンを押すとAlertダイアログで押したボタンが表示されるデモとなっています。

そして、肝心のJavaScriptのコードは、assets/js/microbitble.jsです。

BLE処理Class

今回は、BLEの処理に関して、クラスのような形で実装しています。その中でまずは接続のメソッドの説明をします。

var MicrobitBLE = function() {
  // デバイス情報保持用変数
  this.ble_device = null;

  // ローパスフィルタのための変数
  this.accel_x_before = 0;
  this.accel_y_before = 0;
  this.accel_z_before = 0;
};
MicrobitBLE.prototype = {
  // Class定数定義

  // micro:bit BLE UUID 加速度センサー関連
  ACCELEROMETERSERVICE_SERVICE_UUID: 'e95d0753-251d-470a-a062-fa1922dfa9a8',
  ACCELEROMETERDATA_CHARACTERISTIC_UUID: 'e95dca4b-251d-470a-a062-fa1922dfa9a8',
  ACCELEROMETERPERIOD_CHARACTERISTIC_UUID: 'e95dfb24-251d-470a-a062-fa1922dfa9a8',

  // micro:bit BLE UUID ボタン関連
  BUTTON_SERVICE_UUID: 'e95d9882-251d-470a-a062-fa1922dfa9a8',
  BUTTON_A_CHARACTERISTIC_UUID: 'e95dda90-251d-470a-a062-fa1922dfa9a8',
  BUTTON_B_CHARACTERISTIC_UUID: 'e95dda91-251d-470a-a062-fa1922dfa9a8',

  // micro:bitの加速度センサーデータ書き換え期間
  ACCELEROMETERPERIOD: 5,

  // micro:bitのセンサー値のローパスフィルタ用設定値
  ACCELEROMETER_ALPHA: 0.9,

  // 接続関数(Promiseによる非同期逐次処理(.thenの部分))
  connect: function() {
    navigator.bluetooth.requestDevice({ // デバイスの検索
      filters: [{
        namePrefix: 'BBC micro:bit',
      }],
      // 使いたいSERVICEのUUIDを列挙する
      optionalServices: [this.ACCELEROMETERSERVICE_SERVICE_UUID, this.BUTTON_SERVICE_UUID]
    })
    .then(device => {
      this.ble_device = device;
      console.log("device", device);
      // GATTサーバへの接続
      return device.gatt.connect();
    })
    .then(server =>{
      console.log("server", server)
      // Promise.allは、全てを並列処理して、全てが終わったら次に進む
      return Promise.all([
        server.getPrimaryService(this.ACCELEROMETERSERVICE_SERVICE_UUID),
        server.getPrimaryService(this.BUTTON_SERVICE_UUID)
      ]);
    })
    .then(service => {
      console.log("service", service)
      return Promise.all([
        service[0].getCharacteristic(this.ACCELEROMETERPERIOD_CHARACTERISTIC_UUID),
        service[0].getCharacteristic(this.ACCELEROMETERDATA_CHARACTERISTIC_UUID),
        service[1].getCharacteristic(this.BUTTON_A_CHARACTERISTIC_UUID),
        service[1].getCharacteristic(this.BUTTON_B_CHARACTERISTIC_UUID)
      ]);
    })
    .then(chara => {
      console.log("ACCELEROMETER:", chara)
      alert("BLE接続が完了しました。");
      var buffer = new Uint8Array(2);
      buffer[0] = this.ACCELEROMETERPERIOD & 255;
      buffer[1] = this.ACCELEROMETERPERIOD >> 8;
      chara[0].writeValue(buffer);
      chara[1].startNotifications();
      chara[1].addEventListener('characteristicvaluechanged',this.onAccelerometerValueChanged.bind(this));

      //ボタンが押された際の通知を有効化し、ボタンクリックイベントにコールバックを設定
      chara[2].startNotifications();
      chara[2].addEventListener('characteristicvaluechanged', this.onchangeABtn.bind(this));
      chara[3].startNotifications();
      chara[3].addEventListener('characteristicvaluechanged', this.onchangeBBtn.bind(this));
    })
    .catch(error => {
      alert("BLE接続に失敗しました。もう一度試してみてください");
      console.log(error);
    });
  },
  disconnect: function() {
    if (!this.ble_device || !this.ble_device.gatt.connected) return ;
    this.ble_device.gatt.disconnect();
    alert("BLE接続を切断しました。")
  },
  onAccelerometerValueChanged: function(event) {
    // 6byteの値から2byteずつ切り出す
    accel_x = event.target.value.getInt16(0, true);
    accel_y = event.target.value.getInt16(2, true);
    accel_z = event.target.value.getInt16(4, true);
    console.log("Accelerometer data converted: x=" + accel_x + " y=" + accel_y + " z=" + accel_z);

    // おまけ:ローパスフィルタでノイズをある程度除く
    this.accel_x_before = this.ACCELEROMETER_ALPHA * this.accel_x_before + (1.0 - this.ACCELEROMETER_ALPHA) * accel_x;
    this.accel_y_before = this.ACCELEROMETER_ALPHA * this.accel_y_before + (1.0 - this.ACCELEROMETER_ALPHA) * accel_y;
    this.accel_z_before = this.ACCELEROMETER_ALPHA * this.accel_z_before + (1.0 - this.ACCELEROMETER_ALPHA) * accel_z;
    console.log("Smoothed accelerometer data: x=" + this.accel_x_before + " y=" + this.accel_y_before + " z=" + this.accel_z_before);
  },
  onchangeABtn: function() {
    console.log("A Button");
  },
  onchangeBBtn: function() {
    console.log("B Button");
  }
};

接続処理

上記のうち、connect()が該当箇所となります。BLEの接続手順は非同期逐次処理となるので、Promiseを用いた記述が便利です。そして今回は、複数のSERVICE、複数のCHARACTERISTICSを用いるため、それぞれの接続手順を行うのに、Promise.all([])を用いています(詳細は後述します)。
先に述べた接続手順に対して、プログラムのどの部分が対応しているかを見てみましょう。

  1. DEVICEの検索(このとき、特定の名前のパターンや、必要とするSERVICEをもっているかなどのフィルタリングをかけて検索します)

    navigator.bluetooth.requestDevice({ // デバイスの検索
      filters: [{
        namePrefix: 'BBC micro:bit',
      }],
      // 使いたいSERVICEのUUIDを列挙する
      optionalServices: [this.ACCELEROMETERSERVICE_SERVICE_UUID, this.BUTTON_SERVICE_UUID]
    })
    
  2. DEVICEのGATTサーバへの接続

    .then(device => {
      this.ble_device = device;
      console.log("device", device);
      // GATTサーバへの接続
      return device.gatt.connect();
    })
    

     前段の処理結果は、この.then()の中で書かれているdeviceに格納され、それ以降の処理で利用されます。ここでは最終的にデバイスに対してgatt.connect()を行い、その結果を次の手順に渡すこととなります。
     disconnectする時のためにdevice情報は保存しておきます。

  3. GATTサーバからSERVICEの取得

    .then(server =>{
      console.log("server", server)
      // Promise.allは、全てを並列処理して、全てが終わったら次に進む
      return Promise.all([
        // 
        server.getPrimaryService(this.ACCELEROMETERSERVICE_SERVICE_UUID),
        server.getPrimaryService(this.BUTTON_SERVICE_UUID)
      ]);
    })
    

     前段の処理結果はserverに格納され、次の処理としてGATTサーバに対してgetPrimaryService()で使いたいSERVICEのUUIDを指定しSERVICEの取得を行います。
     ここでは、Promise.all([])を返すことで、全ての取得が完了してから次の処理に進むようにしています。
     この時にそれぞれでgetPrimaryService()するのではなく、getPrimaryServices()でこのデバイスで提供されている全てのSERVICEを取得することもできますが、その場合は戻りの順番がわかりませんので、戻り値の中から必要なSERVICEを見つける手順が次のステップで必要となります。ここではSERVICEが2つ程度なので、個別に取得するようにしています。

  4. SERVICEからCHARACTERISTICSの取得

    .then(service => {
      console.log("service", service)
      return Promise.all([
        service[0].getCharacteristic(this.ACCELEROMETERPERIOD_CHARACTERISTIC_UUID),
        service[0].getCharacteristic(this.ACCELEROMETERDATA_CHARACTERISTIC_UUID),
        service[1].getCharacteristic(this.BUTTON_A_CHARACTERISTIC_UUID),
        service[1].getCharacteristic(this.BUTTON_B_CHARACTERISTIC_UUID)
      ]);
    })
    

     前段でのPromise.all([])の処理の結果は、serviceに格納され渡されますが、複数の処理の結果が渡ってくるので、serviceは配列となっています。
     ここでも同じく、それぞれのサービス(servive[0]とservice[1])に対して、複数のCHARACTERISTICSを取得するため、Promise.all([])を返すことで、全てのCHARACTERISTICSの取得ができてから次の処理に移るようになっています。

  5. CHARACTERISTICSから値を取得したり、CHARACTERISTICSに値を書き込んだりする

    .then(chara => {
      console.log("ACCELEROMETER:", chara)
      alert("BLE接続が完了しました。");
      var buffer = new Uint8Array(2);
      buffer[0] = this.ACCELEROMETERPERIOD & 255;
      buffer[1] = this.ACCELEROMETERPERIOD >> 8;
      chara[0].writeValue(buffer);
      chara[1].startNotifications();
      chara[1].addEventListener('characteristicvaluechanged',this.onAccelerometerValueChanged.bind(this));
    
      //ボタンが押された際の通知を有効化し、ボタンクリックイベントにコールバックを設定
      chara[2].startNotifications();
      chara[2].addEventListener('characteristicvaluechanged', this.onchangeABtn.bind(this));
      chara[3].startNotifications();
      chara[3].addEventListener('characteristicvaluechanged', this.onchangeBBtn.bind(this));
    })
    

     前段で取得したCHARACTERISTICSはcharaに配列として格納されており、それに対して、処理を追加しています。
     この例では、「ACCELEROMETER PERIOD CHARACTERISTICS」という加速度センサーの情報の更新タイミング設定のためのCHARACTERISTICSに、writeValue()で値を送信しています。
     また、値を取得できるCHARACTERISTICSに対しては、addEventListener()で、characteristicvaluechangedのイベントを追加し、それぞれ値が変化した時に実行すべきメソッドを指定しています。この時コールバック指定に.bind(this)を付けているのは、イベントによってメソッドが呼ばれるのでオブジェクトが伝わらないので、thisを渡すためです。
     実際の値の利用についてはこのコールバックに指定したメソッドの説明の中で触れます。

値を活用する

 ここではonAccelerometerValueChanged()を例に説明します。
 値の取得に関する箇所はここです。

    // 6byteの値から2byteずつ切り出す
    accel_x = event.target.value.getInt16(0, true);
    accel_y = event.target.value.getInt16(2, true);
    accel_z = event.target.value.getInt16(4, true);
    console.log("Accelerometer data converted: x=" + accel_x + " y=" + accel_y + " z=" + accel_z);

 データはevent.target.valueとして、characteristicvaluechangedイベントのvalueとして受け取ることができます。そして、そのデータにどのように必要なデータが入っているか、micro:bitのBluetooth Profileのドキュメントを見ると、

Accelerometer Data : Contains accelerometer measurements for X, Y and Z axes as 3 unsigned 16 bit values in that order and in little endian format. Data can be read on demand or notified periodically. Values are in the range +/-1000 and in milli-newtons.

とあります。
 ですので、届いたデータをgetInt16でsignedの16bitデータとして切り出していきます。getInt16()は引数に「開始位置」と「littleEndianかどうか」を渡します。これで加速度のデータを取得できます。

最後に

 micro:bitは2,000円ちょっとで買える割にそこそこ高機能でかつ開発環境もかなり整っています。そんなマイコンをBLE経由でWebサービスのコントローラにすることができます。micro:bitのプログラムはWeb上のブロックエディタで行い、JavaScriptを書くだけでマイコンを活かしたWebアプリケーションを作ることができます。「IoTとか興味あるけど、難しそうだなぁ」って思ってるJavaScript書きの皆さん、これを機に始めてみませんか。