1.BluetoothとBLEについて
Bluetoothは、2.4GHz帯の電波を使用したデジタル機器用の近距離無線通信技術です。この帯域はWi-Fiや電子レンジなども使用している混雑した周波数帯ですが、80MHzの周波数帯を1MHz幅(BLEでは2MHz幅)に分割したチャンネルと、周波数ホッピングという技術で、混信を避けるように絶えず周波数を切り替えながら通信します。かなり以前からワイヤレスのキーボードやマウスなどで使われていましたが、しだいに通信速度が高速になり省電力性能が向上していて、最近ではワイヤレスヘッドフォンやファイル転送など、連続したデータの高速伝送にも使用されています。
BLEはBluetoothの一部で、バージョン4.0から追加された低消費電力の通信規格です。Bluetoothと同じ周波数帯域と技術を使用しますが、低消費電力用途のために作られた新しい規格です。従来規格との互換性よりも消費電力の削減が優先されているので、それまでのBluetoothとの互換性はありません。これを区別するために、それ以前のものはクラシックBluetoothと呼ばれています。
BLEではデータ転送速度は低下し、データ伝送のパケットサイズも非常に小さくなりましたが、それに代えて大幅な省電力化が図られました。IoTやウェアラブルデバイスでは、電池寿命を延ばすために無線部分の消費電力削減が重要課題です。この点で、ボタン電池で1年以上も駆動できるBLE通信技術が、IoTのセンサーデバイスなどで注目されることになりました。
現在の主流はバージョン4.2で、通信速度は1Mbps、通信距離は約100mです。最新のバージョン5.0では、高速モードの速度が2Mbpsになり到達距離も400mに拡大するなど、さらに利用範囲が広がりそうです。
Bluetoothのコア仕様はBluetooth SIG (Special Interest Group)で維持されています。
2.BLE通信の概要
(1)物理的な特性
○周波数とチャンネル
2.40 GHzから2.48GHzまでの80MHzの帯域を、2MHz幅で40のチャンネルに分割します。40のチャンネルには、それぞれ0から39までの番号が付けられています。チャンネルは2種類に分かれていて、37~39までの3チャンネルをアドバタイジング・チャンネル、0~36までの37チャンネルをデータ・チャンネルと呼びます。
アドバタイジング・チャンネルは、デバイスの探索と接続に使います。データ・チャンネルは、接続が完了したデバイス間での通信に使います。
○ホストコントロール
Bluetoothのアーキテクチャーは、コントローラーとホスト、そしてアプリケーションの3つに分かれています。コントローラは電波の送受信、パケット通信、接続管理を行います。ホストは、プロトコルやプロファイルごとの複雑な通信機能を担います。BLEでは、インテリジェンスの大半がコントローラに配置されます。これにより、ホストは長い時間にわたってスリープ状態を維持することができるようになります。何らかのアクションが必要な場合だけ、コントローラによってウエイクアップされるので、大幅な電力の節約が可能になります。
○レイテンシー(反応時間)
BLEは数ミリ秒という短い時間で、接続セットアップとデータ転送に対応できます。したがって、短いバースト状通信なら、わずか数ミリ秒の間に接続を確立してデータを転送し、素早く接続を切ることができます。
(2)BLE通信の仕組み
①アドバタイジングとスキャニング
BLE通信では、センサーデバイスなどのように主としてデータを送るデバイスと、データを受信したりタイミングを制御するデバイス(スマートフォンなども)から構成されます。前者はスレーブやペリフェラル、後者はマスターとかセントラルなどと呼ばれます。
BLE通信の大きな特徴は、スレーブが間欠的に発信している電波をマスターが発見して、目的のスレーブからの信号であれば通信を開始するという方法にあります。
先に述べたコントローラーをもう少し細かく見ると、無線通信で相手とやり取りする物理層と、それを制御するリンク層で構成されています。そして、このリンク層には次の5つの状態があります。
・スタンドバイ(Standby)
・アドバタイジング(Advertising)
・スキャニング(Scanning)
・イニシエーティング(Initiating)
・コネクション(Connection)
マスターは、スキャンして得た情報から接続先を決めてイニシエーティング状態に遷移します。イニシエータは、接続したいスレーブからのアドバタイジング・パケットを受信した後に、接続要求を送信してコネクション状態に移行します。一連の通信が終了すると、マスターはディスコネクト(Disconnect)で切断します。
マスターが実行するスキャニングには、パッシブ・スキャン(Passive Scan)とアクティブ・スキャン(Active Scan)があります。パッシブ・スキャンはアドバタイジング・パケットを受信するだけのものです。これに対してアクティブ・スキャンは、アドバタイジング・パケットを受信した後に、アドバタイザーであるスレーブにリクエストを送り、さらに追加情報を取得します。
②ネットワーク・トポロジー
BLEデバイスは、ブロードキャストとコネクションという2つの形態で通信することができます。どちらも、後で取り上げるGAP(General Access Profile)によって規定された方式です。
○ブロードキャスト方式
あるBLEデバイスから別のBLEデバイスに対して、一方的にデータを発信する通信形態です。データを発信するデバイスをブロードキャスター、データを受信するデバイスをオブザーバーと呼びます。
ブロードキャスターは、一定の周期でアドバタイジング・パケットを送信し続けます。パケットのアドバタイジング・データ部には、仕様に決められた範囲内で自由にデータを設定することができます。ここに、温度や湿度などのデータを格納して、不特定多数のオブザーバーに対して同時に同じデータを発信できるのが特徴です。このような方式なので、機密性を要求されるようなデータのやり取りには向いていません。
この方式の延長線上に、Bluetooth Low Energy beacon(ビーコン)があります。アップルのiBeaconはその一種で、店舗に設置した発信側のビーコン端末と、ビーコン端末からの受信に反応するiPhoneアプリが動作して、商品やセール情報をプッシュ通知したり、店舗側でユーザーの動線分析データを取得することができる仕組みです。
○コネクション方式
あるBLEデバイスと別のBLEデバイスとの間で、相互にデータを送受信する通信形態です。ブロードキャスト方式と異なり、データの送受信は、コネクションに参加したデバイス間のみでプライベートに行われます。
コネクションを開始する側のデバイスをセントラルまたはマスターと呼びます。一方、セントラルからのコネクション開始要求を受け付け、定められたタイミングでデータの送受信をおこなうデバイスをペリフェラルあるいはスレーブと呼びます。
コネクション方式では、後述のGATT(Generic Attribute Protocol)で定義されたデータ構造を利用して、多様な通信を行うことができます。
3.BLEのパケットフォーマット
BLEには1種類のパケットフォーマットと、2種類のパケット(アドバタイジング・パケットとデータ・パケット)しか存在しません。これによって、プロトコルスタックの実装が大幅に簡略化されています。プロトコルスタックは、通信を実現するための一連の通信プロトコル群のことで、後で細かく吟味することになりますが、それに先だって、パケットの内容を見ておきましょう。
(1)アドバタイジング・パケット
まずはアドバタイジング・パケットです。下図は、BLEパケットとアドバータイジングPDU(Protocol Data Unit)の基本的なフォームです。8ビットを1オクテット(Oct)と表記しています。
パケットの基本フォーマットは上段のようにシンプルなものです。先頭のプリアンブルは、信号の強さと読み取るタイミングの検出に使われます。それに続くアクセス・アドレスは32ビットです。各スレーブ向けの全パケットに対して32ビットのアドレスを使用しているので、何十億個ものデバイスを接続することが可能になります。アドバータイズチャネルの場合は、ビット表記で
'10001110100010011011111011010110' (0x8E89BED6)
です。これは、受信側でパケットの判別に使うためと、さらに、受信側でバイト単位のタイミングを取るために使われます。また最後のCRCは誤り検出符号で、巡回冗長検査(Cyclic Redundancy Check)が行われます。
アドバータイジングPDUは、
・アドバータイズ(デバイス情報を通知)
・スキャン要求(デバイス情報を要求)
・スキャン応答(デバイス情報の応答)
・接続要求
などに使用されます。
アドバタイジング・パケットのPDU(Protocol Data Unit)は2オクテットのヘッダーと、デバイスを識別するためのアドレスとして6オクテットが割り当てられています。ヘッダーはPDUと呼ばれ、以下のような意味をもっています。
PDU | 役 割 | 略 語 | 意 味 |
0000 | Connectable undirected advertising | ADV_IND | 接続可能アドバタイジング |
0001 | Connectable directed advertising | ADV_DIRECT_IND | 特定相手限定アドバタイジング |
0010 | Non connectable undirected advertising | ADV_NONCONN_IND | 非接続アドバタイジング |
0011 | Scan request | SCAN_REQ | スキャン要求 |
0100 | Scan response | SCAN_RSP | スキャン応答 |
0101 | Connection request | CONNECT_REQ | 接続要求 |
0110 | Scannable undirected advertising | ADV_SCAN_IND | スキャン可能アドバタイジング |
アドバタイジング・データは、長さ情報をもつフィールドを設置することで任意のデータを指定できますが、31オクテットを超えることはできません。長さ(Length)、AD Type、AD Dataの部分はAD structureという単位でまとめることができ、31オクテットを超えない範囲で繰り返して指定することができます。ただし、ペイロード(正味のデータ部分)は29オクテットと小さいので注意が必要です。
AD Typeは、AD Dataのデータ種別を表すもので、Bluetooth SIGにおいて決められています。いくつも種類がありますが、その主なものは次の通りです。
名 称 | AD Type | 長さ(Oct) | 概 要 |
Flags | 0x01 | 1 | GAP通信における制御情報 |
Shorted Local Name | 0x08 | 可変長 | デバイスの名称(短縮版) |
Complete Local Name | 0x09 | 可変長 | デバイスの名称(完全版) |
Tx Power Level | 0x0A | 1 | 送信電力 |
Manufacturer Specific Data | 0xFF | 可変長 | 任意の送信データ |
(2)データ・パケット
下図のように、データも同様にパケットでやり取りされます。
データPDUは、
・アプリケーションデータの通信
・切断通知
・暗号化要求、応答
・接続パラメータの交換
などに使用されます。
データPDU末尾のMICは、暗号化されたパケットで使用されるMessage Integrity Checkフィールドです。Bluetooth 4.1では最大ペイロードサイズが27バイトでしたが、4.2になって251バイトになり、Lengthの場所とビット数も変わりました。図はバージョン4.2の場合を示します。
パケットのハンドリングで気をつけるのは、最終的に使用できるペイロードのサイズです。下図は、上位層によってペイロードが縮小される様子を示しています。アドバタイジング・パケットでは、スタック上位のプロトコルによってさらに小さくなるケースがあり、最大長が20バイトまでに制限されることがあるので注意しなければなりません。
(3)アドバタイジング・データ
もう一度アドバタイジング・パケットに戻って、アドバタイジング・データの種別を表すAD Typeの主なものについて、その設定情報を掲げておきます。
○Flags(0x01)
デバイスの発見や接続の機能を示すのがFlagsです。
ビット0/1はそれぞれ論理値false/trueに対応します。Flagsはアドバタイジング・パケットに1つだけ含めます。
ビット位置 | 意 味 |
0 | LE Limited Discoverable Mode |
1 | LE General Discoverable Mode |
2 | BR/EDR Not Supported |
3 | Simultaneous LE and BR/EDR to Same Device Capable (Controller) |
4 | Simultaneous LE and BR/EDR to Same Device Capable (Host) |
5..7 | Reserved |
Simultaneous LE and BR/EDR to Same Device Capable は、ホストとコントローラいずれも'0'になります。
ここでBR/EDRとはBluetooth Basic Rate/Enhanced Data Rateのことで、Bluetoothはこの2つとBLEで構成
されています。BR/EDRとLEは同居が可能ですが互換性はありません。
Limited Discoverable Modeは、デバイスを発見できる時間制限があることを示します。接続が切れたから
といって、不用意にアドバタイジングをさせたくない場合に使います。
General Discoverable Modeは、常にデバイスが発見できるモードを示します。通常はこのモードを使います。
○Local Name(0x08/0x09)
ユーザ・インタフェースの表示名などに使われる、ユーザが読める デバイスの名称を示します。
これはアドバタイジング・データかスキャン・データのいずれかに1つだけ含めます。
文字列はUTF-8で符号化されます。C⾔語の’\0’のような、文字列の終端記号は必要ありません。
AD Type | 記 述 |
0x08 | Shortened local name |
0x09 | Complete local name |
ローカルネームが29バイトよりも大きいと、収まりません。この場合には、Shortened local nameを使います。
完全なローカルネームは、上位層のGATTを通して、Device name characteristic から読み出せます。
○Manufacturer Specific Data(0xFF)
アドバタイジング・パケットでセンサーの計測値など少量データを送信する場合に使用します。
AD type は 0xFFです。続くAd Dataは、先頭2オクテットにBluetooth SIGが企業に発行した識別子、そして
任意長のバイナリ・データが続きます。位置ビーコンのような、非接続で周囲の不特定多数のBluetooth LE
デバイスに同報するときにも、データを格納することができます。
なお、企業識別子2オクテットの部分は、以下の条件でテスト用識別子として0xFFFFを使用できることがアナ
ウンスされています。
〔 Bluetooth specification: Company Identifiers 〕
⇒ https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers
「This value (0xFFFF) may be used in the internal and interoperability tests before a
Company ID has been assigned. This value shall not be used in shipping end products.」
○Tx Power Level(0x0A)
送信電力を表します。単位はdBmで、-127 から +127dBm までの値を示します。
Tx Power Levelは、アドバタイザーとの距離の推定に使います。この値と受信したパケットの受信信号強度
(Received Signal Strength Indication, RSSI)から、伝搬損失(pass loss)を求めることができます。
4.BLEのプロトコルスタック
プロトコルスタックは、通信プロトコル(規約や手順)を階層構造で表したものです。Bluetoothのアーキテクチャはコントローラーとホスト、それにアプリケーションから成り立っていますが、その構成別にプロトコルスタックをとらえると、ごく大まかに図のように表すことができます。なお、この図ではHostとControllerの間に位置するHCI(Host Controller Interface)は省略しています。
アプリケーション開発との関わりが強いGeneric Access Profile(GAP)とGeneric Attribute Profile(GATT)については次節で取り上げることにして、その他の部分を簡単に眺めておきましょう。
①物理層(Physical Layer)
アナログ信号の復調と変調、アナログ通信の部分が対象です。主な機能は次のようなものです。
・チャンネル分割
・周波数ホッピング
・デバイス検出と接続の確立
・ブロードキャスト送信
・接続機器間の双方向通信
ちなみに、周波数ホッピングは次の公式から求められるチャンネルへホップします。ただし、ホップ数の値はコネクション確立時に通知されるので、新たな確立の都度異なります。
新チャンネル = (現在のチャンネル + ホップ数) mod 37
②リンク層(Link Layer)
規格で定義されたタイミング制約のすべてを担う部分であり、標準インターフェイスを用いて複雑さとリアルタイム要件を他の層から隠しています。
リンク層では次の役割が定義されています。
・アドバタイザ
アドバタイズパケットを送信するデバイス。
・スキャナ
アドバタイズパケットを受信するデバイス。
・マスター
コネクションを開始し、その後管理するデバイス。
・スレーブ
コネクション要求を受け付け、マスターのタイミングに従うデバイス。
さらに、以下のような機能を担っています。
・ホストから指示されたデバイスアドレスの設定。
・アドバタイジングとスキャンのタイミングとスキャン方法の制御。
スキャンについては前述のパッシブ・スキャンとアクティブ・スキャンの手順制御。
・コネクション確立とホワイトリスト機能。
ホワイトリストは相手先を選択するためのフィルタリング機能です。
・暗号化
暗号鍵はホストで生成・管理されるが、それに基づく暗号化と復号化を行います。
③論理リンク制御およびアダプテーション・プロトコル(Logical Link Control and Adaptation Protocol)
L2CAPは2つの機能を受け持ちます。
そのひとつは、上位層から複数のプロトコルを受け取ってBLE標準パケットフォーマットへ変換(および逆変換)するもので、プロトコル多重化装置として働きます。
また、フラグメント化と再結合も行います。これは送信側では、上位層からの大きなパケットをBLEパケットの最大ペイロードサイズである27バイトに収まる塊に分割します。受信側では、フラグメント化された複数のパケットを、1つの大きなパケットへ再結合し、それを上位層の適切なエンティティへ送信します。
アプリケーション開発上では、L2CAPパケットヘッダーが4バイト占有することに注意が必要です。デフォルトパケットサイズを利用する場合、実効的なユーザーペイロード長は23バイトになります。
④アトリビュート・プロトコル(Attribute Protocol)
デバイスの提示するアトリビュートに基づいた、単純なクライアント/サーバー方式のステートレスなプロトコルです。
多くのアプリケーションは、接続が完了するとマスター(セントラル)とスレーブ(ペリフェラル)の一方がサーバーになり、もう一方はクライアントになります。サーバーとクライアントはそれぞれ属性(Attribute)を持っていて、それぞれが属性のやりとりをするのがATTプロトコルです。これはクライアントとサーバ間の一対一の通信プロトコルで、専ら属性を確認するためのものです。アプリケーションが必要とするデータのやりとりは、さらに上位のプロファイルが担うことになります。
ATTでは、Attributeのアクセスに関する6種類の方法が採用されています。
(下図は『IoT技術情報サイト Tech Web』から引用)
RequestとResponse、IndicationとConfirmationはそれぞれ対になっています。クライアントとサーバーが同時に行えるやりとりは一つだけなので、Request(Indication)はResponse(Confirmation)を受け取るまで、次のRequest(Indication)を送ることはできません。これによって、フロー制御などを必要とせずにやり取りすることが可能になっています。
⑤セキュリティ・マネージャ(Secutity Manager)
セキュリティ暗号鍵を生成し交換するために設計された、一連のセキュリティアルゴリズムとプロトコルの両方に対応します。
次の2つの役割が定義されています。
・イニシエータ(Initiator)
常にリンク層のマスターと対応し、したがってGAP役割のセントラルです。
・レスポンダー(Responder)
常にリンク層のスレーブと対応し、したがってGAP役割のペリフェラルです。
セキュリティ・マネージャには、次の3つの手順へのサポートが提供されています。
・ペアリング(Pairing)
セキュアな暗号化されたリンクへスイッチできるように、一時的な共通セキュリティ暗号鍵を生成する
手順です。この一時鍵は保存されないのでその後のコネクションで再利用はできません。
・ボンディング(Bonding)
ペアリングの後に、永続的なセキュリティ暗号鍵の生成と交換が行われるシーケンスです。
永続鍵は不揮発性メモリーへ保存され、2つのデバイス間に永続的なボンド(結合)が作り出されるので、
その後のコネクションでは再度ボンディングを行うことなくセキュアなリンクが迅速に確立できます。
・暗号化再確立(Encryption Re-establishment)
ボンディングが完了した後、暗号鍵は両方で保存しておくことができます。暗号鍵が保存されている場合、
この手順に定義されている方法で、再びペアリングまたはボンディング手順を行う必要がなく、これらの
暗号鍵を用いてセキュアなコネクションを再確立できます。
5.2つの汎用プロファイル
(1) GAP(Generic Access Profile:汎用アクセス・プロファイル)
デバイス検索、コネクション、セキュリティの確立など、異なるメーカーのデバイス間での相互運用性を保証し、データのやり取りが行えるようにするための制御手順を規定するものです。次のような事項が規定されています。
・役割(Roles)
振る舞いに関する要件。
・モード(Modes)
操作モードとそれらの遷移。
・手順(Procedures)
一貫した相互運用可能な通信を実現するための手順。
・セキュリティ(Security)
セキュリティのモードと手順。
・その他(GAP Data format)
アドバタイジング・データのフォーマット。
ここでは主なものだけ(きわめて主観的ですが)を見ておきましょう。
○役割
・ブロードキャスター(Broadcaster)
データを定期的に配布する送信のみの用途に特化した役割。
アドバタイズパケットでデータを送信するため、待ち受け状態にあるあらゆるデバイスで利用できる。
リンク層のアドバタイザ役割を利用する。
・オブザーバー(Observer)
ブロードキャストしているデバイスから、データを収集する受信のみの用途に特化した役割。
リンク層のスキャナ役割を利用する。
・セントラル(Central)
リンク層のマスターに対応する。
常にコネクションのイニシエーターとなり、デバイスをネットワークに参加させる機能。
他のデバイスのアドバタイズパケットを待ち受け、選択したデバイスとのコネクションを開始する。
このプロセスを繰り返すことによって、1つのネットワークに複数のデバイスを参加させることができる。
・ペリフェラル(Peripheral)
リンク層のスレーブに対応する。
アドバタイズによってセントラルに見つけてもらい、次にセントラルとのコネクションを確立する。
○モードと手順: ブロードキャストモードとオブザベーション手順
ブロードキャスターとなったデバイスは、1台以上のオブザーバーに一方的にデータを送信できる。
どのオブザーバーにデータが届いたかをブロードキャスターが知る方法はない。
またオブザーバーは、実際に受信できるという保証なしに、存在するかも知れないブロードキャスターを一時的に、
あるいは無期限に待ち受ける。
○その他
前述の「BLEのパケットフォーマット」で取り上げたAD Typeは、GAPのコア規格で定義されています。
これは長さ(1Byte)、AD Type(1Byte)、実際のデータ(可変長)からなる、データ構造体のシーケンスで
構成されています。
(2) GATT(Generic Attribute Profile:汎用アトリビュート・プロファイル)
データの送受信やデータの構造について定義したプロファイルで、全てのデータ定義の基本となっています。データ層の最上位定義で、すべてのBLEデバイスはGATTで定義されている仕様に従ってデータのやり取りを行われなければなりません。言い換えると、BLEアプリケーションは、すべてこのGATTを使用して構築されることになります。
○役割
他のプロトコルやプロファイルと同様に、GATTでもデバイスの役割が定義されています。
・クライアント(Client)
クライアントは要求をサーバーへ送信し、サーバーからの応答を受信する。
まずサービス検索を行って、サーバーのアトリビュートの存在と性質について問い合わせを行う。
サービス検索が完了した後、サーバーに見つかったアトリビュートの読み出しや書き込みを行う。
・サーバー(Server)
サーバーはクライアントから要求を受信し、応答を返す。
サーバーはアトリビュートに整理された形でユーザーデータを保存し、クライアントが利用できるようにする。
○UUID(Universally Unique Identifier)
後で説明するサービスやキャラクタリスティックなどの要素にアクセスするために、BLEでは名前をつけるのではなく、ユニバーサル固有識別子(UUID)を対応付けて扱います。
UUIDは重複しない16バイトのIDです。これには、Bluetooth SIGが割り当てた「定義済UUID」があります。
0000XXXX-0000-1000-8000-00805f9b34fb
XXXXの2Byteだけが異なり、他の部分は同じ内容です。Service UUIDの場合はXXXXが180fならBattery Serviceを、1809ならHealth Thermometerを意味します。また定義済のCharacteristic UUIDでは、XXXXが2a19ならBattery Levelを表します。これらの定義済UUIDを使う場合は、16Byte表記を省略してXXXXの部分だけを2Byteで短縮表記して通信データ量を削減することができます。これらはいずれもBluetooth SIGで定義済みのものであり、この値をユーザーが勝手に他のことに使うことはできません。
ユーザーが独自のServiceやCharacteristicを作成する場合は、16ByteのオリジナルUUIDを作成しなければなりません。これはWebブラウザーから次のサービスを利用することで、簡単に生成することができます。
⇒ https://www.uuidgenerator.net/
version1とversion4がありますが、どちらを使用してもかまいません。
○アトリビュート(Attribute)
アトリビュートはGATTで定義される最小のデータエンティティーです。GATTやATTはアトリビュートしか扱えないため、すべての情報をこの形式に整理しておく必要があります。これは、次の情報から構成されています。
・ハンドル
すべてのアトリビュートにユニークな16ビットの識別子です。
アトリビュートをアクセスするために使用するもので、複数のトランザクションに渡って変化しません。
・タイプ
アトリビュートのタイプには常にUUIDが設定されます。2バイト、4バイト、16バイトを占有します。
後述のGATT階層構造でのレイアウトを決める標準UUID、データ種別を規定するプロファイルUUID,他にも
ベンダー固有UUIDなど多くの種類があります。
・パーミッション
アトリビュートに対してどのような操作やセキュリティが許容されるかの要件が設定されます。
例えば操作については、None、Readable、Writable、Readable and writableなどが設定されます。
・値
実際のデータの内容が格納されます。
データ型には何の制約もありません。
○アトリビュートとデータの階層構造
次にGATTの階層構造を表します。厳密な階層構造を規定して、クライアントとサーバー間の情報のアクセスや
検索が行えるようにしています。
・Service
デバイスの機能を表すもので、Peripheralには1つ以上のServiceがあります。
ハンドル: 0xNNNN
タイプ: 次の3種類のいずれか。
0x2800 - UUID primary service
0x2801 - UUID secondary service
0x2802 - UUID include
パーミッション: Readable
値: サービスUUID
・Characteristic
Serviceを構成する特性でデータを格納するために用いられます。
Serviceには1つ以上のCharacteristicがあります。
CharacteristicはValue、Property、Descriptorで構成されています。
ハンドル: 0xNNNN
タイプ: 0x2803
パーミッション: Readable
値: 値ハンドル(0xMMMM)、プロパティ、Character UUID
・Value
BLEでやり取りするデータです。
ハンドル: 0xMMMM
タイプ: Character UUID
パーミッション: 任意
値: 実際の値(可変長)
・Propaty
データの操作が下表のどれに対応しているかを示します。必要なものを合成して指定できます。
名 前 | 値 | 意 味(それぞれのビットが設定されている場合) |
Broadcast | 0x01 | AD Typeで指定して値をアドバタイジング・パケットに入れることができる |
Read | 0x02 | クライアントからの値の読み込みが可能 |
Write Without Response | 0x04 | クライアントから値の書き込みが可能 |
Write | 0x08 | 上と同様に書き込み可能だが、サーバーからのレスポンスを要求する |
Notify | 0x10 | サーバーがクライアントにcharacteristicの変更を通知できる |
Indicate | 0x20 | 上と同様に通知できるが、クライアントからのレスポンスを要求する |
Signed Write Command | 0x40 | クライアントからの署名付き書き込みが可能 |
Extended Properties | 0x80 | descriptorのCharacteristic Extended Propertiesを使用できる |
Queued Write | ** | クライアントはキューイング書き込みを行える |
Writable Auxiliaries | ** | クライアントはDescriptorへの書き込みが行える |
・Descriptor
追加情報の記述子です。主にクライアントへメタデータ(追加的情報)を提供するために使われます。
GATT定義ディスクリプタとプロファイルまたはベンダー定義ディスクリプタの2種類があります。
GATT定義ディスクリプタの詳細は、次のリンク[GATT Defined Descriptors]をクリックしてください。
GATT定義ディスクリプタのなかで最もよく使われるのは次の通りです。
Name | 値 | 意 味 |
Characteristic Extended Properties Descriptor | 0x2900 | Characteristic Propertyの拡張プロパティと合わせて使用する。Queued Write / Writable Auxiliariesを指定できる。 |
Characteristic User Description Descriptor | 0x2901 | Characteristicの説明文字列。ユーザーが可読できる記述(UTF-8文字列)が含まれる。 |
Client Characteristic Configuration Descriptor | 0x2902 | CCCDと略され、最も重要でよく使われるディスクリプタ。2ビットのフィールドから成り、NotifyとIndicateの許可を制御する。 |
Characteristic Presentation Format Descriptor | 0x2904 | Format,Exponent,Unit,Namespaceで構成され、Valueのフォーマットを指定する。利用できるフォーマットは、論理型、文字列型、整数型、浮動小数型、汎用の型付けされていないバッファーなど。 |
6.Arduino IDE ESP32 BLE Library
以上で、BLE通信アプリケーションを開発するために必要な基本事項を網羅的に取り上げました。
ESP32でのアプリケーション開発では、Neil Kolban氏が開発した「Arduino IDE ESP32 BLE Library」を使用します。とても良くできたライブラリーで、BLEの多様な機能をモデル化した多くのクラスから構成されています。
このライブラリーは、本シリーズ①準備作業編で、「Arduino core for the ESP32のBLE」をインストールしていればすでに組み込まれています。
ここでは、ライブラリーの概要について、開発者自身が公開している「BLE C++ Guide.pdf」を要約します。ポイントだけをピックアップし意訳しているので、ぜひ原典の一読をお勧めします。
なお以下では、Peripheralをサーバー、Centralをクライアントと表現しているので注意してください。
(1)BLE サーバー
○BLEサーバーの構成
BLEサーバーは図のように構成されています(『BLE C++ Guide.pdf』から引用)。
ここでのBLEサーバーは、BLEペリフェラルとも呼ばれるものです。
BLEサーバーは1つ以上のサービスで構成されています。
サービスは1つ以上のキャラクタリスティックで構成され、キャラクタリスティックにゼロないし1つ以上のディスクリプタがあります。
○BLEライブラリ-のクラス
BLEライブラリーでは、図のBLEサーバーを次のようなクラスで構成しています。
・BLEServer: Serverモデルです
・BLEService: Serviceモデルで、BLEServerに所属
する
・BLECharacterstic: Characteristicモデルで、
BLEServiceに所属する
・BLEDescriptor: Descriptorモデルで、
BLECharacteristicに所属する
さらに
・BLEAdvertising: Serverに所属し、他にサーバー
が存在することをアドバタイジングする
○BLEサーバーの擬似コード
最小のBLEサーバーは次のような擬似コードになります。
// Initialize the BLE environment BLEDevice::init("ServerName"); // Create the server BLEServer* pServer = BLEDevice::createServer(); // Create the service BLEService* pService = pServer->createService(ServiceUUID); // Create the characteristic BLECharacteristic* pCharacteristic = pService->createCharacteristic(CharacteristicUUID, properties); // Set the characteristic value pCharacteristic->setValue("Hello world"); // Start the service pService->start(); |
このように、デバイスを初期化してまずサーバーを作成し、サービスを作成し、キャラクタリスティックを作成します。そして値をセットして、着信要求に応答するようにサービスを開始します。
サーバー起動後には、BLEAdvertisingオブジェクトにアドバタイジングを指示することができます。
BLEAdvertising* pAdvertising = pServer->getAdvertising(); pAdvertising->start(); |
アドバタイジングにはいくつもの方法があり、これを設定するためにBLEAdvertisementDataクラスを利用することができます。次にこれを例示します。
BLEAdvertising* pAdvertising = pServer->getAdvertising(); BLEAdvertisementData advertisementData; // set the properties of the advertisement data // See the setter methods of the BLEAdvertisementData class pAdvertising->setAdvertisementData(advertisementData); pAdvertising->start(); |
○コールバック機能
BLECharacteristicの内容であるvalueは、読み取り要求や書き出し要求に従って読み書きすることができます。これを行うには、C++のクラスをサブクラス化できる機能を利用します。BLECharacteristicCallbacksというコールバックのクラスがその機能を提供しています。
・onRead(BLECharacteristic* pCharacteristic)
クライアントから読み取り要求が届くと呼び出されます。キャラクタリスティックの新しい値は関数から
戻る前に設定することができ、クライアントによって受信された値として使用されます。
・onWrite(BLECharacteristic* pCharacteristic)
クライアントが書き込みを開始したときに呼び出されます。キャラクタリスティックと独自のコードには
すでに新しい値が設定されているので、それを読み取って必要な処理をすることができます。
class MyCallback: public BLECharateristicCallback { void onRead(BLECharacteristic* pCharacteristic) { // Do something before the read completes. } void onWrite(BLECharacteristic* pCharacteristic) { // Do something because a new value was written. } }; |
これを使うためには、次のようにBLECharacteristicに上記のコールバックハンドラーを登録します。
pCharacteristic->setCallbacks(new MyCallback()); |
以下に、クライアントが値を要求するたびに起動からの時間を送信する例を示します。
class MyCallbackHandler: public BLECharacteristicCallbacks { void onRead(BLECharacteristic* pCharacteristic) { struct timeval tv; gettimeofday(&tv, nullptr); std::ostringstream os; os << "Time: " << tv.tv_sec; pCharacteristic->setValue(os.str()); } }; |
同じように、サーバーについてもコールバックを設定することができます。
それはBLEServerCallbacksからサブクラス化されていて、次のような仮想メソッドを備えています。
・onConnect(BLEServer* pServer)
接続が発生したときに呼び出される。
・onDisconnect(BLEServer* pServer)
切断が発生したときに呼び出される。
これらを利用して、センサーを有効にしたり無効にしたり、また切断時にエネルギーを消費しないようにコントロールすることなどが可能になります。
BLEサーバーについてのもう一つの要件として、データを相手方に「プッシュ」することが挙げられます。何かが起きたときに、サーバー側からクライアントに非同期(一方的)に信号を送る方法です。この操作は「notify」と呼ばれ、キャラクタリスティックの値が変化したことをクライアントに通知することで、クライアントは変化が起こったことを知ることができます。
サーバー上では、次のコマンド指示します。
pCharacteristic->notify();
次のように、キャラクタリスティックに設定した値を通知すれば、その値を相手方に送信できます。
pCharacteristic->setValue("HighTemp");
pCharacteristic->notify();
notify()に似た機能に indicate()があります。notify()は一方通行で確認を受け取りませんが、indicate()は送信後の確認応答を要求します。
indications/notificationsに関連して、「Client Characteristic Configuration」というBLE記述子があります。これは UUIDに 0x2902という値を持ち、NotificationとIndicationを管理するビットフィールドを提供します。これによって、サーバーとクライアント間の通信を合理的に行うことが可能になります。
BLE仕様では、notificationあるいはindicationで送信できるデータの最大量が20バイト以下に制限されています。この点は注意しなければなりません。もしそれを超える場合は、最初の20バイトだけが送信されます。
キャラクタリスティックでは値をバイナリーで保持します。C++言語の機能を利用して、独自のモデルを作成できます。また、BLECharacteristicのサブクラスとして下部をカプセル化することができます。
例えば、キャラクタリスティックが温度を表していれば次のようにできます。
class MyTemperatureCharacteristic: public BLECharacteristic { MyTemperature(): BleCharacteristic(BLEUUID(MYUUID)) { setTemperature(0.0); } void setTemperature(double temp) { setValue(&temp, sizeof temp); } double getTemperature() { return *(double*)getValue(); } }; |
(2)BLEクライアント
ESP32をクライアントにする場合は、サーバーがアドバタイジングしている情報をスキャンします。スキャンを実行すると、常にアドレスを含む小さなサイズのレコードを受け取ります。ライブラリーではBLEScanというクラスにモデル化しています。
このクラスのインスタンスを取得するには、次のように BLEデバイスに要求します。
BLEScan* pMyScan = BLEDevice::getScan();
実際のスキャンを開始するには、start()メソッドにスキャン時間を引数として実行させます。時間の単位は秒です。これはブロッキングコールなので、指定時間が経過後に戻ってきます。
pMyScan.start(30); // Scan for 30 seconds
スキャンで見つかった情報は、BLEResultsクラスのインスタンスから取得することができます。getCount()でいくつの結果が得られたかを取得、またgetDevice(index)でアドバータイジングされたデバイスを知ることができます。
スキャン結果でコールバック関数を実行することができます。そのためには、start()を呼び出す前にBLEAdvertisedDeviceCallbacksをオーバーライドしてコールバック関数を定義します。これにはonResult()というメソッドがあり、見つかったときに実行する処理を指定できます。またonResult()の引数で指定するBLEAdvertisedDeviceにより、見つかったデバイスの性質を知ることもできます。
class MyCallbacks: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { // Do something with the found device ... } }; BLEDevice::init(""); BLESan* pMyScan = BLEDevice::getScan(); pMyScan->setAdvertisedDeviceCallbacks(new MyCallbacks()); pMyScan->start(); |
注意しなければならないのは、このonResult()はスキャンが見つけたすべてのデバイスに対して呼び出されることです。その際に、そのBLEAdvertisedDeviceのインスタンスが渡され、対象のBLEアドレスを知ることができます。
アドバタイズされたデバイスから通知される可能性のある属性は次の通りです。
・Appearance
・Manufacturer data
・Name
・RSSI
・Primary service UUID
・Transmit power
BLEAdvertisedDeviceのメソッド」に代えて、もっと簡潔に希望する処理を実行する方法があります。 例えば次の通りです(これはほんの一例ですが)。
if (advertisedDevice.hasServiceUUID()) { BLEUUID service = advertisedDevice.getServiceUUID(); if (service.equals(BLEUUID((uint16_t)0x1802) { // we found a useful device … } } |
接続対象のデバイスが見つかったのでスキャンを終了したい場合は、次のようにします。
advertisedDevice.getScan()->stop();
これらの結果から、getAddress()を実行することで接続したいサーバーのデバイスアドレスを取得することができます。
BLEクライアントは BLEClientクラスとしてモデル化されています。次のコードで、新しい BLEClientのインスタンスを作成することができます。
BLEClient* pMyClient = BLEDevice::createClient();
続いて対象とするデバイスへの接続を要求します。
pMyClient->connect(address);
connect()はブロッキングコールなので、戻ったときには接続されています。しかし、相手に接続するだけでは十分でなく、何らかの処理をするのが目的です。すでに見てきたように、相手のサーバーには1つ以上のサービスがあり、各サービスには値を保持する1つ以上のキャラクタリスティックが存在しています。これらを識別するためには、BLERemoteServiceと BLERemoteCharacteristicモデルを使用します。
BLEサーバーに接続したら希望する BLERemoteServiceへの参照をリクエストできます。
BLERemoteService* pMyRemoteService = pClient->getService(serviceUUID);
サービスへの参照を取得したら、必要なキャラクタリスティックへの参照を指定します。
BLERemoteCharacteristic* pMyRemoteCharacteristic =
pMyRemoteService->getCharacteristic(characteristicUUID);
このようにして、最終的にキャラクタリスティックの値を操作することが可能になります。
値を読むには、
std::string myValue = pMyRemoteCharacteristic->readValue();
値を書くには次のようにします。
pMyRemoteCharacteristic->writeValue("abc");
これらをまとめると以下のようになります。
// Create the client BLEClient* pMyClient = BLEDevice::createClient(); // Connect the client to the server pMyClient->connect(address); // Get a reference to a specific remote service on the server BLERemoteService* pMyRemoteService = pClient->getService(serviceUUID); // Get a reference to a specific remote characteristic owned by the service BLERemoteCharacteristic* pMyRemoteCharacteristic = pMyRemoteService->getCharacteristic(characteristicUUID); // Retrieve the current value of the remote characteristic. std::string myValue = pMyRemoteCharacteristic->readValue(); |
void notifyCallback( BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* data, size_t length, bool isNotify) |
この機能は、BLEサーバーが通知メッセージを送信したときに呼び出されます。関数が呼び出されると、サーバーから送信されたデータの長さなど、キャラクタリスティックに関連した参照を取得できます。
registerForNotify()による登録を解除したい場合は、もう一度registerForNotifyを呼び出します。その場合はコールバック関数にNULLを渡します。