ESP32-WROOM-32EでMicroPythonを使い開発(BLE編1)

〔マイコンのトップに戻る〕
[準備編] [module編] [GPIO編] [通信編] [I2S編] [WiFi編]   [BLE編2] [BLE編3] [色々編]


 今回ESP32で"Bluetooth"を動作せる為に基礎仕様を少し調べてみました、結構難解でぇ、
良く解らないまま記述しているのでぇ、間違っていると思いますがぁ...ご勘弁を m-(_~_)-m 。
又、よそ様のHP記事をパクっている箇所が有りますがぁ...作者様如何かご勘弁を  (。-人-。) 。

 "ubluetooth-低レベル Bluetooth"のクラス説明はこちらを参照しましょう。
このモジュールは、ボード上の Bluetooth コントローラーへのインターフェースを提供します。
このモジュールは Bluetooth Low Energy(BLE)をサポートしています。
尚、「このモジュールはまだ開発中であり、そのクラス、関数、メソッド、定数は変更される可能性が
あります」だそうです。

《BLE》

BLEでは、ピアツーピア通信(1対1)だけでなくブロードキャスト通信(不特定多数に向けた通信)も可能。
"Bluetooth 5.0"では、BLEを拡張してメッシュネットワーク対応機能が追加されているらしい。

BLEには、4つの役割(ロール)があります。

Peripheral(ペリフェラル)
 ・接続を要求する通信パケット(ブロードキャスト通信)”アドバタイズ”を送信します。
  接続完了時はスレーブとして動作。
 ・例)スマートウォッチやワイヤレスイヤホン等のBluetoothデバイス。
Central(セントラル)
 ・”アドバタイズ”をスキャンし、ペリフェラル機器と接続します。
  接続完了時はマスターとして動作。
 ・例)スマートフォン、タブレット、PC等の情報端末機器

この1対1の通信接続を確立した後に、何時でもGATT通信(データパケット通信)が可能です。
”アドバタイズ”通信の詳しい話はこちらを参照しましょう。
アドバタイズパケット”の構成を見たい人はこちら、パケットの解析はこちらを参照しましょう。
この”アドバタイズ”通信の次は"ペアリング(セキュリティ鍵交換)"が開始され、
ペアリングが完了し、交換鍵を保存して置く事をボンディングと呼びます。
ボンディングされていれば次回からの接続が同じデバイスならペアリング無しで、保存鍵を使いデータを
暗号化できます。

Broadcaster(ブロードキャスター)
 ・ブロードキャスト通信用の”アドバタイズ”を送信します。(送信するだけです)
 ・例)温度センサ、位置ビーコン等。
Observer(オブザーバー)
 ・ブロードキャスト通信用で、”アドバタイズ”をスキャンするが接続しない。(受信するだけです)
 ・例 : 表示機器、スマートフォン等。

ObserverとBroadcasterは、ブロードキャスト通信を行う時に利用されますが、接続/ペアリングが
行われていない為に暗号化通信はサポートされず、BroadcasterからObserverへの送信しか出来ません。
(単方向通信)

Bluetoothの電波強度

クラス
出力
到達距離
Class 1
100mW
(技適:10mW)
100m
Class 2
2.5mW
10m
Class 3
1mW
1m


《プロトコルスタック》

プロトコルスタックの概念図
プロトコルスタック図

コントローラー層
コントローラー層には、PHY、LinkLayer があります。

PHY
 無線インターフェイスは、2.4GHz帯(2402-2480MHz)を、2MHz幅で40(0-39Ch)の周波数チャネルに
 分け、利用する周波数チャネルをランダムに変える「周波数ホッピング」を行いながら、
 最大3Mbpsの速度で10 - 100mの距離で無線通信を行います。
 ユーザーデータパケットは 0 - 36 のチャネルを使用して送信されます。
 アドバタイジングパケットは37、38、39 のチャネルを使用して送信されます。
 周波数ホッピング(AFH:Adaptive Frequency Hopping)は、他の機器との相互干渉・混信を避ける為の
 技術です。詳しくはここを参照しましょう。

LinkLayer
 PHY と直接やり取りし、無線のリンク状態を管理します。
 L2CAPからのパケットを暗号化し送信します、受信したパケットの復号化も行います。
 マスター、スレーブ、アドバタイザー、スキャナー等の、デバイスの役割を決定し各パケットの送受信を
 行います。

ホスト層
ホスト層には、GAP、L2CAP、SMP、ATT、GATT があります。

GAP
 GAPは他の全てのプロファイルの基礎として機能するプロファイルで、機器の検索、機器の接続、等の
 接続確立を行い、又、認証、暗号化等の管理を行うもの。

SMP
 SMPは、データパケットの暗号化と復号化にセキュリティアルゴリズムを適用します。
 ペアリングと秘密鍵の交換をするプロトコルで、キー(暗号化やID等)を生成して格納します。

ATT
 接続処理が完了すると、マスタ(セントラル)とスレーブ(ペリフェラル)になって、通常はスレーブ側が
 サーバーになり、マスタ側がクライアントになります。
 クライアント側からアクセスし、属性(Attribute)というデータをやり取りします。
 各Attributeの値を読み書きする為にFind、Read、Writeの3つの機能が提供されています。
 詳しい説明は、こちらを参照しましょう。

GATT
 属性(Attribute)というデータを集めて、ある機能を実行する為のデータベースとしてこれを、
 アプリケーション間でやり取りします。
 アプリケーションは、全てこのGATTを使用して構築されるのでデータ転送の主軸となります。
 ある機能とは、センサの値読み出しや機器の操作指示等で全て属性の集まりです。
 詳しい説明は、こちらを参照しましょう。
 クライアント側(マスタ)
  クライアントは要求をサーバーへ送信し、サーバーからの応答を受信します。
  先ずは、サービス検索を行って、サーバーのアトリビュートの存在と性質(機能)について問い合わせを
  行います。
  サービス検索が完了した後、サーバーで見つかったアトリビュートの読み出しや書き込みを行います。
 サーバー(スレーブ)
  サーバーはクライアントからの要求を受信し、応答を返します。
  サーバーはアトリビュートを整理された形でユーザーデータを保存し管理を行い、クライアントが
  利用出来る様にします。

L2CAP
 ATT と SMP で指定されたリンク構成に従いデータをBLEパケット形式にカプセル化し送信します。
 受信したパケットからデータを取り出します。
 パケットの分割・再構成機能・受信確認・再送機能があります。
 マスタ側に主導権があった、通信のインターバルやスループットがスレーブ側で変更出来ます。

アプリケーション層
アプリケーション
 APP層は、色んなアプリケーション間の相互運用性を提供するプロファイルを定義する直接の
 ユーザー インターフェイスです。

アドバタイズ通信は、[アプリケーション]->[GAP]->[SMP]->[L2CAP]->[LL]->[PHY]と伝わります。
データ通信は、[アプリケーション]->[GATT]->[ATT]->[L2CAP]->[LL]->[PHY]と伝わります。

プロトコルスタックの詳しい内容はこちらを参照しましょう。

《データパケット》

パケットはバイナリー型式で、各フィールドはビッグエンディアンで格納されています。
各パケットは、ヘッダーペイロードで構成されます、”データパケット”の構成はこちらです。
ヘッダーは2byteでストリームID、トランザクションID等が含まれます。
ペイロードは31byte未満のストリームで、アプリケーションの要件に基づいて変化します。
(Bluetooth 4.2からは、ペイロードは251byteになっているらしいです)
先ずは、こちらでデータ接続の基本概要を読みましょう。

インターバル通信
BLE通信は、最小7.5ms(最大4s)のインターバル間隔で間欠的に通信を行います。
(インターバル間隔は1.25ms単位で設定可能)
BLEデバイスは消費電力を抑える為に、ある一定の時間間隔で起きて通信し又寝る、と言った動作を
繰り返しています。

通信は、マスター主導で行われ、マスターからは、インターバルタイミングで空のパケットをスレーブに
送信して来ます。或いは、データの送信要求等もこのインターバルタイミングで送信して来ます。
スレーブは、その送信に対して空のパケットで応答しますが、マスターは一定時間(4s:変更可)応答が
無ければ接続を切断します。
なのでスレーブは切断された場合は、新しい接続を許可する為にアドバタイジングを再開始しましょう。

services と characteristics の検出
@ クライアントは、サーバー(ペリフェラル)がどの様な機能を持っているか検出要求を実行します。
A サーバーは、自分が持つ全ての属性のハンドル一覧を返します。
B 検出が完了すると、クライアントはアクセスしたい属性のハンドルを使って、
  サーバーに読み書き要求を送信し始める事が出来ます。
C サーバーは、要求された属性に従い動作して応答を返します。

属性やサービスにキャラクタリスティックの話はこちらの"BLEのデータ構造"以降を読みましょう、
或いは、こちらを参照しましょう。
尚、データ通信は"GATT"で行われるので、こちらの"GATT(Generic Attribute Profile)概要"を
参照しましょう。

《"ubluetooth"のクラス用語集》

MicroPythonのESP32に構築されているBluetoothスタックは、"BTstack"と言う物らしい。
後ぉ、"NimBLE"スタックも出来るらしい("NimBLE-Arduino")。

〔BLE.config〕

アドレス
ブルートゥース機器を識別する為に使用されるのがアドレス(48ビット:6Oct)です。
アドレスが使用されるのはアドバタイズ”通信の時だけで、データパケット通信以降はアクセス・アドレスを使います。

アドレスは、Public AddressRandom Addressの2種類(アドレスタイプ:addr_type)が有り、
Random Addressには、更にResolvable Private AddressesNon-Resolvable Private Addresses
有ります("Static Address"もあるみたいだが、"ubluetooth"のクラスには記載が無い)。

アドレスモード(addr_mode)
 0x00 - PUBLIC - デバイス固有の物理アドレスを使います。
     機器の製造時に書き込まれる固定のアドレス(IEEE機関割り当て)です、
     MACアドレスの様な物です。
 0x01 - RANDOM - 電源投入のたびに生成される乱数アドレスを使います。
     各端末で乱数により自動生成されます。
 0x02 - RPA - Resolvable Private Addresses を使います。
     乱数で自動生成されますが、接続ごとにID解決キー(IRK)と呼ばれる鍵を使い生成します、
     受信した側もIRKによりアドレスの正当性を検証します。
 0x03 - NRPA - Non-Resolvable Private Addresses を使います。
     デバイスを識別できない一時的なアドレスです。

デフォルトのアドレスモードは、デバイスが固有の物理アドレスを持っている場合 PUBLIC になり、
持っていない場合は RANDOM になります。

ATT MTU(mtu)
GATT層のパケットでペイロード部分のサイズ長さの事で、変更可能です。
デフォルト値は23Byte、最大251バイトまで大きくする事が出来ます。
ATT MTU交換は、マスター側から"Exchange MTU Request"で自分のMTU値を送り、スレーブ側は
このMTU値と自分のMTU値を比較して小さい方の値を"Exchange MTU Response"で応答します。

MITM-protection(mitm)
ペアリング時のMITMプロテクションの有効/無効を決めます。
有効にすると、MITM攻撃(中間者攻撃)からの防御が出来る様になります。
(6桁のパスキー認証コードがMITM保護付きになるかどうかの設定だと思います)

デバイスの I/O 機能(io)
自分のBLEデバイスが持つI/O能力(ペアリングを実行する為の能力)を設定します。

 _IO_CAPABILITY_DISPLAY_ONLY
  6桁の数値を表示可能なディスプレイ装置を持つ(Input装置なし)
 _IO_CAPABILITY_DISPLAY_YESNO
  6桁の数値を表示可能なディスプレイ装置と"Yes"/"No"の確認入力機能を持つ
 _IO_CAPABILITY_KEYBOARD_ONLY
  0'〜'9'の数値の入力機能を持つ(Output装置なし)
 _IO_CAPABILITY_NO_INPUT_OUTPUT
  一切のInput装置、Output装置を持たない
 _IO_CAPABILITY_KEYBOARD_DISPLAY
  ディスプレイ装置と入力機能を持つ

LESCペアリング(le_secure)
"LE Secure" ペアリングが必要かどうかを設定します。
デフォルトは false です(即ち "Legacy Pairing" になります)。
Bluetooth 4.0で導入されたペアリング方法で、"Legacy Pairing"と呼ばれています。
Bluetooth 4.2から導入されたセキュリティの強い方法で、鍵交換に公開鍵暗号方式が使われます。
詳しい話はこちらの"Bluetooth Low Energyのペアリングとボンディングについて"を参照しましょう。

〔イベント〕

イベントコードの定数は、こちらを参照しましょう。
def bt_irq(event, data):
    if event == _IRQ_CENTRAL_CONNECT:
        # セントラルがこの周辺機器に接続しました。
        conn_handle, addr_type, addr = data
    elif event == _IRQ_CENTRAL_DISCONNECT:
        # セントラルがこの周辺機器から切断しました。
        conn_handle, addr_type, addr = data
    elif event == _IRQ_GATTS_WRITE:
        # クライアントがこのcharacteristic 又はdescriptorに書き込みました。
        conn_handle, attr_handle = data
    elif event == _IRQ_GATTS_READ_REQUEST:
        # クライアントが読み取りを発行しました。注:これはSTM32でのみサポートされています。
        # 読み取りを拒否する場合はゼロ以外の整数を返し、
        # 読み取りを受け入れる場合はゼロ(又は無し)を返します。
        conn_handle, attr_handle = data
    elif event == _IRQ_SCAN_RESULT:
        # シングルスキャン結果。
        addr_type, addr, adv_type, rssi, adv_data = data
    elif event == _IRQ_SCAN_DONE:
        # スキャン期間が終了したか、手動で停止しました。
        pass
    elif event == _IRQ_PERIPHERAL_CONNECT:
        # gap_connect()は成功しました。
        conn_handle, addr_type, addr = data
    elif event == _IRQ_PERIPHERAL_DISCONNECT:
        # 接続されている周辺機器が切断されました。
        conn_handle, addr_type, addr = data
    elif event == _IRQ_GATTC_SERVICE_RESULT:
        # 接続されたデバイスがサービスを返しました。
        conn_handle, start_handle, end_handle, uuid = data
    elif event == _IRQ_GATTC_SERVICE_DONE:
        # サービス検出が完了すると呼び出されます。
        # 注:ステータスは成功するとゼロになり、それ以外の場合は実装固有の値になります。
        conn_handle, status = data
    elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
        # gattc_discover_services()によって検出された各characteristicに対して呼び出されます。
        conn_handle, def_handle, value_handle, properties, uuid = data
    elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
        # キャラクタリスティック検出が完了すると呼び出されます。
        # 注:ステータスは成功するとゼロになり、それ以外の場合は実装固有の値になります。
        conn_handle, status = data
    elif event == _IRQ_GATTC_DESCRIPTOR_RESULT:
        # gattc_discover_descriptors()によって検出されたdescriptorごとに呼び出されます。
        conn_handle, dsc_handle, uuid = data
    elif event == _IRQ_GATTC_DESCRIPTOR_DONE:
        # ディスクリプタ検出が完了すると呼び出されます。
        # 注:ステータスは成功するとゼロになり、それ以外の場合は実装固有の値になります。
        conn_handle, status = data
    elif event == _IRQ_GATTC_READ_RESULT:
        # gattc_read()が完了しました。
        conn_handle, value_handle, char_data = data
    elif event == _IRQ_GATTC_READ_DONE:
        # gattc_read()が完了しました。
        # 注:value_handleはbtstackではゼロになります(但し、NimBLEには存在します)。
        # 注:ステータスは成功するとゼロになり、それ以外の場合は実装固有の値になります。
        conn_handle, value_handle, status = data
    elif event == _IRQ_GATTC_WRITE_DONE:
        # gattc_write()が完了しました。
        # 注:value_handleはbtstackではゼロになります(但し、NimBLEには存在します)。
        # 注:ステータスは成功するとゼロになり、それ以外の場合は実装固有の値になります。
        conn_handle, value_handle, status = data
    elif event == _IRQ_GATTC_NOTIFY:
        # サーバーが通知要求を送信しました。
        conn_handle, value_handle, notify_data = data
    elif event == _IRQ_GATTC_INDICATE:
        # サーバーが指示要求を送信しました。
        conn_handle, value_handle, notify_data = data
    elif event == _IRQ_GATTS_INDICATE_DONE:
        # クライアントが指示を確認しました。
        # 注:確認応答が成功するとステータスはゼロになり、それ以外の場合は実装固有の値になります。
        conn_handle, value_handle, status = data
    elif event == _IRQ_MTU_EXCHANGED:
        # ATT MTU交換が完了しました(自機器又はリモートデバイスのいずれかによって開始されました)。
        conn_handle, mtu = data
    elif event == _IRQ_L2CAP_ACCEPT:
        # 新しいチャンネルが承認されました。
        # 接続を拒否する場合はゼロ以外の整数を返し、受け入れる場合はゼロ(又は無し)を返します。
        conn_handle, cid, psm, our_mtu, peer_mtu = data
    elif event == _IRQ_L2CAP_CONNECT:
        # これで、新しいチャネルが接続されました(接続又は受け入れの結果として)。
        conn_handle, cid, psm, our_mtu, peer_mtu = data
    elif event == _IRQ_L2CAP_DISCONNECT:
        # 既存のチャネルが切断されている(ステータスがゼロ)か、接続の試行が失敗しました(ゼロ以外のステータス)。
        conn_handle, cid, psm, status = data
    elif event == _IRQ_L2CAP_RECV:
        # 新しいデータがチャネルで利用可能です。l2cap_recvintoを使用して読み取ります。
        conn_handle, cid = data
    elif event == _IRQ_L2CAP_SEND_READY:
        # Falseを返した以前のl2cap_sendが完了し、チャネルを再度送信する準備が整いました。
        # ステータスがゼロ以外の場合、送信バッファがオーバーフローした為、アプリケーションはデータを再送信する必要があります。
        conn_handle, cid, status = data
    elif event == _IRQ_CONNECTION_UPDATE:
        # リモートデバイスの接続パラメータが更新されました。
        conn_handle, conn_interval, conn_latency, supervision_timeout, status = data
    elif event == _IRQ_ENCRYPTION_UPDATE:
        # 暗号化状態が変更されました(ペアリング又はボンディングの結果である可能性があります)。
        conn_handle, encrypted, authenticated, bonded, key_size = data
    elif event == _IRQ_GET_SECRET:
        # 保存されたシークレットを返します。
        # keyがNoneの場合、このsec_typeのインデックス番目の値を返します。
        # それ以外の場合は、このsec_typeとキーに対応する値を返します。
        sec_type, index, key = data
        return value
    elif event == _IRQ_SET_SECRET:
        # このsec_typeとキーのシークレットをストアに保存します。 
        sec_type, key, value = data
        return True
    elif event == _IRQ_PASSKEY_ACTION:
        # ペアリング時のパスキー要求に応答します。
        # 詳細については、gap_passkey()を参照して下さい。
        # アクションは、構成された「io」構成と互換性のあるアクションになります。
        # アクションが「数値比較」の場合、パスキーはゼロ以外になります。
        conn_handle, action, passkey = data



[WiFi編へ] [ページ上へ] [BLE編2へ]


【きむ茶工房ガレージハウス】
Copyright (C) 2006-2021 Shigehiro Kimura All Rights Reserved.