続・mbed の環境で Bluetooth LE アプリを書いてみる

投稿者: | 2015年3月25日

How to modify an mbed BLE demo code to my own objective.

今回はもう少しちゃんと、mbed BLE アプリの書き方を説明してみます。と言っても、mbed のサイトに「BLE アプリの書きかた」という説明がある訳でもなく(少なくとも 1ヶ月前はなかった)、さらに C++ 言語のクラス設計の知識が必要になってきますので、やや敷居が高いかも知れません。また私もサンプルコードを見ながら書いてますので、説明が間違っていたらごめんなさい。もし実際に Nordic nRF51 で BLE アプリを御所望の場合は、お問い合わせください。(← 営業ですか!)

前回も書きましたように、手本にするには BLE_HeartRate というサンプルコードが良いと思います。このままでも RedBear BLE Nano で動作しますが、サンプルコードのバージョンに依っては LED の GPIO が違う可能性があるので、まずはそこを修正しておきましょう。そうすれば、少なくともアプリがクラッシュすると LED が停止する(可能性が高い)ので動作が分かりやすいです。

ちなみに、mbed ではサンプルコードのバージョンがどんどんアップデートしてしまうので、以下の私のコードと最新版では内容が違う可能性があります。(というか、実際に変わってました。) 私の参考にしたのは Revision 54 というものですが、現在は 56 になっているようです。

変更ですが、

DigitalOut led1(なんとか);

という部分を見つけて、

DigitalOut led1(P0_19);

と書き換えます。(BLE Nano の LED は、GPIO 19番なのです。) 繰り返しになりますが、mbed のほうで BLE Nano のピン割り当てに(ヘッダファイル定義などで)合わせてくれているかも知れないので、変更前から LED が点滅している場合は、特に変更の必要はないでしょう。(関係ないですけど、どうして BLE Nano の LED は、ドングルに差すと見えなくなる向きに付いているんでしょうか?)

閑話休題。次は、自分で考えた BLE サービス(GATT プロファイルとかいうやつ)を組み込む方法についてです。BLE_HeartRate サンプルプログラムを取り込む(import する)と、BLE_API/services というディレクトリの下に、BLE の仕様で定義された各種の標準サービスが定義されていることが分かります。例えば HeartRateService.h というファイルがありますが、これは Bluetooth.org で定義されている org.bluetooth.service.heart_rate というサービス(UUID: 0x180D)に対応しています。

ちなみに、このような標準サービスには 16ビットの UUID が(GattService::UUID_HEART_RATE_SERVICE のように)割り当てられていますが、独自サービスを定義するためには 128ビットの UUID を割り当てなくてはなりません。しかし、試作段階ではいちいち 128ビットの UUID を割り当てるのも面倒ですので、特に気にならなければ、取り敢えずこのような 0x180D を使ったまま試作をするという方法もあります。(私はそうしてました。) なお、BLE ペリフェラルのデバッグをするには、BLE セントラル側も用意しなくてはなりません。一番簡単なのは、皆さんがお持ちのスマートフォンで、次のようなアプリを使うという方法です。(私は iPhone なので、自分の分かる iOS アプリで説明しますが、Android アプリも充実しているようです。)

どちらも一長一短ありますので、両方持っていると効率的だと思います。

話は逸れますが、試作中のアプリで GATT プロファイルの定義を変更したりすると、それがアプリの表示に反映されないことがあります。どうも、GATT プロファイルの定義はアプリ内で持っているのではなく、iOS のプロトコルスタックでキャッシュしているようで、一度 Bluetooth 接続を OS で無効にしてやらないと、うまく定義が反映されないことがあるようです。御参考まで。

さて上記アプリですが、LightBlue の良い点は、ある characteristic を読み出した value の履歴(時刻表示と共に)が残っていることです。また、読み出した値を 16進数だけでなく、10進数や UTF-8 でデコードに切り替えられるのが便利です。

2015-03-25 13.38.01

一方の BLExplr ですが、BLE サービスと BLE characteristic の階層化がうまく可視化されているので、BLE のプロトコルの理解を助けるかも知れません。できたら両方インストールしておくことをお勧めします。なお、Mac OS 上で動く解析ツールも(Xcode か何かで)提供されているようですが、なぜか私の環境では正しく動作しなかったので利用していません。他に、Linux 上の BlueZ 等も有効に使えそうなので、機会があれば紹介したいと思います。

また話を戻します。

という訳で、今回は独自の BLE サービスを実装したいので、既存のサービスをコピーしてくることから始めます。本来は、mbed BLE のほうで「この BLE クラスライブラリをこのようにカスタマイズすれば、あなたのアプリが作れます」という利用マニュアルやテンプレートを用意してくれると嬉しいのですが、私が探したときには見つかりませんでした。そこで、上記で BLE_HeartRate を使っていることもあることから、mbed BLE クラスライブラリから HeartRateService.h を拝借してくることにします。

このサービス org.bluetooth.service.heart_rate は、次のようないくつかの characteristic を持っています。

  • Heart Rate Measurement (0x2A37)
  • Body Sensor Location (0x2A38)
  • Heart Rate Control Point (0x2A39)

詳細は、以下のサイトを参考にしてください。

さて、この 3つの characteristic は HeartRateService.h の中で次のように実装されています。

まずは private メンバとして次の定義があります。

HeartRateValueBytes valueBytes;
uint8_t controlPointValue;

GattCharacteristic hrmRate;
ReadOnlyGattCharacteristic<uint8_t> hrmLocation;
WriteOnlyGattCharacteristic<uint8_t> controlPoint;

上の 2行は実際に値を保持しているメンバ変数で、下の 3行は GATT プロファイルを変数として定義しています。この辺りの理解は Doxygen で生成されたリファレンスマニュアルを参考にするしかないのですが、リファレンスだけでは正直分かりにくいところです。

私の理解では、サイズが予め決まっている読出し専用の characteristic では ReadOnlyGattCharacteristic を、書込み専用では WriteOnlyGattCharacteristic を、読書きしたい場合は ReadWriteGattCharacteristic を、テンプレートを使ってサイズを与えて定義します。サイズが不定の characteristic の定義では、GattCharacteristic を使うように「作られている」ようです。

という訳で、次のような private メンバ変数を定義します。

uint32_t counterValue;
ReadWriteGattCharacteristic<uint32_t> counter;

characteristic に対応するメンバ変数を定義するには、サービス(service)クラスを定義する必要があります。(説明の順番が逆になりました。) 例えば MyCounter サービスを定義したい場合には、次のようなクラスを定義します。上記でコピーしたファイルの中で、HeartRateService というクラス定義を書き換えるのが簡単でしょう。

class MyCounterService {
public:
    MyCounterService(BLEDevice &_ble, uint32_t paramCounter):
        ble(_ble),

        // private メンバ変数のいろんなコンストラクタを呼び出す,

        counter(GattCharacteristic::UUID_HEART_RATE_CONTROL_POINT_CHAR,
                &counterValue,
                GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY) {

ここでは、先の説明の通り UUID_HEART_RATE_CONTROL_POINT_CHAR を借用しています。また、最後に指定している BLE_GATT_CHAR_PROPERTIES_NOTIFY はオプションの追加プロパティで、BLE の Notify 機能(クライアントから問い合わせなくてもサーバー(ペリフェラル)から勝手にアップデートしてくれる characteristic)を使うことを指定するもののようです。

さらに、登録したいサービスと characteristic を BLE スタックに知らせます。次のようなコードです。

GattCharacteristic *charTable[] = {&counter};
GattService myCounterService(GattService::UUID_HEART_RATE_SERVICE,
        charTable,
        sizeof(charTable) / sizeof(GattCharacteristic *));

最後に必要なのは 2つの関数実装です。一つは、onDataWritten というもので、クライアントからサーバーに Write 要求が合った場合に呼び出される関数です。これを書くには、まずは上記の MyCounterService コンストラクタで次のような呼び出しをします。

ble.onDataWritten(this, &GateCountService::onDataWritten);

次に、その関数を定義します。

void onDataWritten(const GattCharacteristicWriteCBParams *params) {
    if (params->charHandle == counter.getValueHandle()) {
        counterValue = params->data[0] << 24 | params->data[1] << 16 (以下略);
    }

    // 以下続く

}

もう一つは、アプリケーションプログラムから BLE スタックに対して値をアップデートする、つまり Read 要求や Notify のための関数です。次のような感じです。

void updateCounter(uint32_t val) {
    counterValue = val;
    ble.updateCharacteristicValue(counter.getValueAttribute().getHandle(),
            (const std::uint8_t*) &counterValue,
            sizeof(counterValue));
}

これで、一つ characteristic を定義できました。他に characteristic が必要な場合は、同じようにコードを書いていきます。多少 C++ 言語が分かれば(私も C++ はあまり詳しくないのですが)、なんとかなるのではないでしょうか。

あとは、実際に BLE クライアントから接続して、動作を確認してみてください。なお、iOS の場合は、BLE characteristic を追加修正する度に OS 設定で Bluetooth を無効化/有効化、するのを忘れずに。。。

今日はここまで。