144Labの入江田です。 BLEアプリケーションをArduinoを使って開発するための細かいノウハウを 何回かの記事にまとめてみます。
おすすめのターゲット
ノルディックのnRF52シリーズをMPUとして持つボードがお勧めです。 Cortex-M4Fを搭載しておりfloat型計算もそこそこがんばれます。 BLEは無線を扱うので技適が取得済みである製品を選択しましょう。
SOC/モジュール
- InsightSiP社のiSP1507,1807シリーズのSOC
- Raytac社のMDBT42,50シリーズのモジュール
- Layrd社のBL652/654シリーズのモジュール
- Minew社のMS50SF/MS88SFモジュール
以上のSOC/モジュール(アンテナ内蔵)搭載のブレイクアウトボードであれば技適取得済みです(一部のモデルや新製品は要確認)。
BLEサポートしたArduinoコアライブラリ
https://github.com/adafruit/Adafruit_nRF52_Arduino
これはArduinoボードサポートにadafruitのnrf52シリーズ対応をインストールした時に 一緒にインストールされます。
BLEにはペリフェラル役とセントラル役の2つがあり、一般にマイコンデバイスがペリフェラルになり、PCやスマホがセントラルという役割分担が多いです。 コネクションタイプとノンコネクションタイプという使い分けも決めておきます。
主にビーコンを作る場合はノンコネクションタイプで、 一般的なデバイスはコネクションタイプになるかと思います。
各抽象名と包含関係
ペリフェラルは機能提供側でセントラルは機能利用側です。 以下の名称とツリー構造はBLEライブラリで抽象的に構築されています。
- ペリフェラル(サーバー)
- プライマリサービス(主たる提供サービス)
- キャラクタリスティックa - キャラクタリスティックb - ...
- オプショナルサービスA
- オプショナルサービスB
- ...
- プライマリサービス(主たる提供サービス)
サービスとキャラクタリスティックには識別用にUUIDをもちます。
サービスにはプライマリかセカンダリ以降か、 ジェネリックサービスかカスタムサービスか、という区別があります。
カスタムサービスはプライマリサービスだけがアドバタイズにUUIDを載せることができるという特徴があり、接続前にプライマリサービスだけはセントラルが識別可能です。その他のカスタムサービスは接続後にしか識別できません。
ジェネリックサービスの場合はUUIDの長さが2バイトです(カスタムは16バイト)。また、そこにぶら下がっているキャラクタリスティック群は既定のものが定義済みかつUUIDが2バイトです。
特性(キャラクタリスティック)ごとの通信のやり方は以下の3通り
それぞれ通信上のACKは返すけれどレスポンスデータを返したりはできないので注意が必要です。
BLE共通のセットアップ
- ヘッダインクルード
#include <bluefruit.h>
- 自動LED制御をオフ このコードを入れておかないとターゲットのLED1の制御をBLEドライバーに奪われてしまいます。(もちろん任せても問題ないなら不要です)
Bluefruit.autoConnLed(false);
- 送信出力電力とデバイス名をセット
Bluefruit.setTxPower(4); // default: 4 dBm Bluefruit.setName("DeviceName");
- BLEドライバーの動作開始
Bluefruit.begin();
BLEボタンペリフェラルの実装
もっとも一般的な形であるコネクション型のペリフェラルでボタン機能提供デバイスを作ってみます。コネクション型ペリフェラルはセントラルと1対1で接続され、セントラルと接続されていない間のみ「アドバタイズ」という自身の名称やサービス情報を周辺にばら撒きます。セントラルは多くの「アドバタイズ」を受信してその中から目的のデバイスを探し出して接続を試みるというような挙動を採るのが一般的です。
ジェネリックなペリフェラルはここの一覧をみて当てはまるものがある場合はそちらのサービスや特性と同じ形にすると良いでしょう。 ボタンに関するジェネリックなサービスは定義されていないのでユーザーカスタムなサービスUUIDを決めます。
- プライマリサービスのセットアップ
ジェネリックなサービスや特性に使われるユニークID識別子は16bitsですが、ユーザーカスタムな識別子は128bits幅のUUID形式が必要です。
ユーザーカスタムなサービスのUUIDはuuidgen
コマンド等で作成しましょう。
ただし、これが一般的かどうかは賛否が分かれますが、
仮に{ccddeeff-0011-2233-4455-66778899aabb}
というUUIDが生成された場合、
3〜4オクテット目の値をゼロにします。結果としてサービスのUUIDは{ccdd0000-0011-2233-4455-66778899aabb}
とします。(理由は後述)
UUIDの表記はビッグエンディアンだけど、メモリにバイト列としておく時はリトルエンディアンである必要があります(BLEマイコンの多くはリトルエンディアンを想定しているものがほとんどです)。
const uint8_t PrimaryServiceUUID[] = { // ccdd0000-0011-2233-4455-66778899aabb 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x00, 0x00, 0xdd, 0xcc, }; BLEService PrimaryService(PrimaryServiceUUID);
セットアップ処理
PrimaryService.begin();
- サービスに特性(キャラクタリスティック)を付与
オリジナルな特性のUUIDを付与する必要があり、uuidgen
で作成しても良いのですがBLE仕様が歴史的に識別子がその前身のANT仕様の16bits幅から128bits幅に拡張された経緯もあり、nRFシリーズではBLEハードの基本は16bits幅のIDでサービスや特性を識別します。
そして16bitsのIDと128bitsのUUIDの対応メモリをsoftdevice用に確保していますがコンパクトな領域しかないので、保持できる128bitsのUUIDの数に限界があります。極力、一つのサービスには一つの128bits幅UUIDをベースにして16bitsの値だけを変化させて割り当てます。
サービスのUUIDが{ccdd0000-0011-2233-4455-66778899aabb}であったので太字(3〜4オクテット目)のところを特性ごとに連番で指定します。こうすることで128bitsUUIDメモリの利用数を節約することができます。 (特にたくさんのサービスや特性を提供しようとする場合には必須となります)
- 特性A
{ccdd0001-0011-2233-4455-66778899aabb}
- 特性B
{ccdd0002-0011-2233-4455-66778899aabb}
- 特性C
{ccdd0003-0011-2233-4455-66778899aabb}
- ...
const uint8_t BtnCharacteristicUUID[] = { // ccdd0001-0011-2233-4455-66778899aabb 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x01, 0x00, 0xdd, 0xcc, }; BLECharacteristic BtnCharacteristic(BtnCharacteristicUUID);
// リードと通知を利用する設定(ライトはできない) BtnCharacteristic.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY); // セキュリティモードに関する設定 BtnCharacteristic.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); // セントラルへ送るデータ長(ここでは1オクテット) BtnCharacteristic.setFixedLen(1); BtnCharacteristic.begin();
- アドバタイズのセットアップ
アドバタイズ開始処理(これ以降セントラルから発見可能になる)
// アドバタイズパケットのフラグ Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); // 電力とデバイス名とプライマリサービスUUIDをアドバタイズ情報に載せる Bluefruit.Advertising.addTxPower(); Bluefruit.ScanResponse.addName(); Bluefruit.Advertising.addService(PrimaryService); // 接続を切ったらアドバタイズを再開する設定 Bluefruit.Advertising.restartOnDisconnect(true); // アドバタイズはファストモードー>スローモードー>ストップという状態遷移 // ファストモードの時間を指定する(30秒) Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast // 何秒間アドバタイズするか(ゼロの場合はノンストップ) Bluefruit.Advertising.start(0);
- ボタン操作に対しボタン特性の更新
void callback() { uint8_t newState = (uint8_t)(1 - digitalRead(button)); // button is active LOW // only notify if button state chagnes if (newState != buttonState) { buttonState = newState; BtnCharacteristic.write8(buttonState); BtnCharacteristic.notify8(buttonState); } } attachInterrupt(button, callback, ISR_DEFERRED | CHANGE);
コード全体
#include <bluefruit.h> #define button 28 const uint8_t PrimaryServiceUUID[] = { // ccdd0000-0011-2233-4455-66778899aabb 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x00, 0x00, 0xdd, 0xcc, }; BLEService PrimaryService(PrimaryServiceUUID); const uint8_t BtnCharacteristicUUID[] = { // ccdd0001-0011-2233-4455-66778899aabb 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x01, 0x00, 0xdd, 0xcc, }; BLECharacteristic BtnCharacteristic(BtnCharacteristicUUID); void startAdvertize() { // アドバタイズパケットのフラグ Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); // 電力とデバイス名とプライマリサービスUUIDをアドバタイズ情報に載せる Bluefruit.Advertising.addTxPower(); Bluefruit.ScanResponse.addName(); Bluefruit.Advertising.addService(PrimaryService); // 接続を切ったらアドバタイズを再開する設定 Bluefruit.Advertising.restartOnDisconnect(true); // アドバタイズはファストモードー>スローモードー>ストップという状態遷移 // ファストモードの時間を指定する(30秒) Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast // 何秒間アドバタイズするか(ゼロの場合はノンストップ) Bluefruit.Advertising.start(0); } uint8_t buttonState; void callback() { uint8_t newState = (uint8_t)(1 - digitalRead(button)); // button is active LOW // only notify if button state chagnes if (newState != buttonState) { buttonState = newState; BtnCharacteristic.write8(buttonState); BtnCharacteristic.notify8(buttonState); } } void setup() { pinMode(button, INPUT_PULLUP); buttonState = digitalRead(button); Bluefruit.begin(); Bluefruit.autoConnLed(false); Bluefruit.setTxPower(4); // default: 4 dBm Bluefruit.setName("BtnPeripheral"); PrimaryService.begin(); BtnCharacteristic.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY); BtnCharacteristic.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); BtnCharacteristic.setFixedLen(1); BtnCharacteristic.write8(buttonState); BtnCharacteristic.begin(); attachInterrupt(button, callback, ISR_DEFERRED | CHANGE); startAdvertize(); } void loop() { delay(20); }
この実装例では全ての処理を割り込みで行なっており、loop関数内をdelayのみでCPU時間のほとんどをdelayで過ごすようにしてあります。
消費電力について
コアライブラリはFreeRTOSベースで作られており、delayはCPUをアイドル状態に遷移させます。 この形を取るとBLEの電波送信の時だけ消費電力が大きく、それ以外はかなり消費電力を下げることができます。
あとは消費電力量はBLE電波送信の頻度でほぼ決まります。
- アドバタイズ(ファストフェーズ)は送信頻度が細かく消費電力大
- アドバタイズ(スローフェーズ)は送信頻度が少なく消費電力中
- 接続状態は接続キープのための送信頻度+アプリの送信頻度だけなので
- アプリではボタン操作した時のみであれば消費電力はかなり小さい
動作の確認
私はよくChrome系ブラウザのWebBluetoothを利用します。
(ただし、現状のWebBluetoothはBLEのセントラルにしかなれません)
以下のような内容のindex.html
を作成し、python3 -m http.server
などして
http://localhost:8000 をChrome系ブラウザ(新EdgeでもOK)で開きます。
<script> const serviceUuid = "ccdd0000-0011-2233-4455-66778899aabb"; const characteristicUuid = "ccdd0001-0011-2233-4455-66778899aabb"; const filter = { filters: [{ services: [serviceUuid] }] }; async function connect() { const device = await navigator.bluetooth.requestDevice(filter); const server = await device.gatt.connect(); const service = await server.getPrimaryService(serviceUuid); const btnCh = await service.getCharacteristic(characteristicUuid); btnCh.addEventListener("characteristicvaluechanged", (ev) => { let value = event.target.value; if (value.getUint8(0)) { document.getElementById("state").innerText = "push!"; } else { document.getElementById("state").innerText = ""; } }); await btnCh.startNotifications(); console.log(btnCh); } </script> <body> <button onclick="connect()">Connect</button><label id="state"></label> </body>
connectを押すとユーザーサービスにマッチするBluetoothデバイス一覧が表示されます。そのひとつを選択してペアリングを行うとデバイスに接続し、デバイスのボタンを押すと通知を受け取ります。上記のコードではボタンの状態に合わせてラベルの表記を変更しています。
ここでWindowsユーザーは注意が必要なんですが、ペアリングをOSの設定画面から一度行なっておく必要があります。
Android,Linux,macOSのChromeはもちろん、ChromeOSも大丈夫でした。また、iOSの場合WebBLE(有料)というブラウザアプリで利用可能です。