M5stack(ESP32) で Bluetooth(btstack) の機能を利用する際の注意点

Page content

M5stack の公式 Web サイトを見ると、 M5stack の開発環境は以下のものが挙げられています。

<https://docs.m5stack.com/en/platform>

  • UIFlow
  • Arduino
  • Camera Series
  • Micropython
  • .NET nanoFramework

これらは、ソフトウェアを簡単に開発することにフォーカスされていて、 Bluetooth を制御する API の充実度は低いようです。

上記の開発環境で提供されていない Bluetooth の機能を利用するには、 ESP32 の official SDK の esp-idf か、 汎用的な Bluetooth Library の btstack を使う必要があります。

M5stack で Bluetooth の機能を利用するアプリを開発する場合、 その機能がどの開発環境で提供されているかを調査し、 その中から使い易い環境で開発を進める、 というようにすると、効率良く開発を進められます。

また、プロファイル自体はサポートされていても、 実際に動かすと思うように動かないことがあるので、 API の有無だけで判断するのではなく、サンプル等を実際に動かして確認する 必要があります。

そして、各開発環境は日々機能が拡張されているので、 最新の環境で調査するべき です。

ここでは、 btstack を使った Bluetooth Keyboard の Device と Host の制御方法について 説明します。

Bluetooth の基本

ここでは、 Bluetooth の制御に必要な基本的な知識をまとめます。

Bluetooth classic と BLE

Bluetooth は、 Version 4.0 で新しい制御方式 BLE (Bluetooth Low Energy) が 追加になりました。 この BLE は、それまでの方式 classic (BR/EDR) と互換性がないもので、 ハードウェア構成も異なります。

Bluetooth のハードウェアモジュールには、以下の構成があります。

  • classic のみサポート (Bluetooth)
  • BLE のみサポート (Bluetooth Smart)
  • classic/BLE 両方サポート (Bluetooth Smart Ready)

なお、市販されている多くの Bluetooth キーボードは基本的に classic です。

ハードウェア構成が違えば、当然それを制御するソフトウェア側にも対応が必要です。

ソフトウェアも、ハードウェアモジュールと同様に以下の構成があります。

  • classic のみサポート
  • BLE のみサポート
  • classic/BLE 両方サポート

ESP32 のハードウェア自体は、classic/BLE 両方サポートしていますが、 Bluetooth ライブラリがどのような対応になっているかは、ライブラリ依存です。

対象の Bluetooth 機器が classic/BLE どちらなのかを調べるには、 スマホ用の Bluetooth 解析アプリを使うと簡単に確認できます。

device と host

Bluetooth には、 keyboard などの機器と、それらを接続して利用する機器があります。

前者を device、後者を host といいます。

これは論理的なソフトウェア制御の違いで、Bluetooth ハードウェアモジュール自体は、 device にも host にもなれます。

classic/BLE と device/host

Bluetooth のソフトウェア制御は、次の 4 つの組み合わせに分けられます。

  • classic の device
  • classic の host
  • BLE の device
  • BLE の host

これは論理的な制御の組み合わせであり、 技術的にはこれらの組み合わせを複数同時に利用することも可能です。

しかし、Bluetooth ライブラリが複数同時利用に対応しているかどうかは別の問題です。

btstack

<https://github.com/bluekitchen/btstack>

btstack は、汎用的な Bluetooth stack ライブラリで、 ESP32 に限らず複数のチップをサポートしています。

btstack は、esp-idf をインストールしている環境に、 btstack のライブラリを追加でインストールして利用する形態になります。

btstack を利用する際のセットアップ方法は、以下を参照してください。

<https://github.com/bluekitchen/btstack/tree/develop/port/esp32>

btstack のソフトウェアデザイン

イベントとコールバック

btstack は、非同期に処理を行なうため、 各種イベントごとにコールバック関数を登録する方式を採用しています。

例えば、「ペアリングの要求を受けた場合にその要求を許可するかどうか」や、 「接続が切断された場合にどう処理するか」をコールバックに登録した関数で処理します。

どのようなイベントがあり、 そのコールバックではどのような情報を取得できるかは、 以下のソースで確認できます。

btstack/src/btstack_event.h

また、そのコールバックでどのような処理が必要なのかは、 各サンプルプログラムを確認すると分かります。

大部分のイベントは、単に状態を通知するためのものであり、 処理しなければならないイベントは極僅かです。

なお、一つのイベントに対して複数のコールバックを登録できるものと、 一つのイベントに対して 1 つのコールバック登録に制限されるものがあります。

ペアリングデータの保存

Bluetooth は、ペアリングで取得した情報を保持しておき、 その情報をもとに再起動などの時に再接続を行ないます。

つまり、この情報は不揮発の情報として保持しておく必要があります。

btstack のライブラリ内には、 これらの情報を FLASH の NVS 領域に保持する処理が書かれているので、 ユーザプログラムで直接それらの情報にアクセスする必要はありません。

一方で、ペアリングの情報を削除したい、というようなケースがあると思います。 その場合、以下の関数を呼ぶと、これらの情報を削除できます。

  • gap_drop_link_key_for_bd_addr( addr );
  • le_device_db_remove( index );

あるいは、少し強引な方法ですが、 NVS の namespace "BTstack" のデータを全て削除し、再起動する方法でも削除可能です。

classic の host

M5stack に classic の keyboard 等を接続して利用する場合、 classic の host 制御が必要になります。

btstack の classic host のサンプルプログラムは以下にあります。

btstack/port/esp32/example/hid_host_demo

このサンプルの中の主な処理は以下です。

  • classic HID ホストの初期化
  • 周辺の classic デバイス有無のスキャン
  • 指定の classic デバイスへのペアリング
  • ペアリング済みの classic デバイスからの接続要求許可

各処理を以降で説明します。

classic HID ホストの初期化

1
2
3
4
5
6
7
    hid_host_init(s_hid_descriptor_storage, sizeof(s_hid_descriptor_storage));
    hid_host_register_packet_handler(packet_handler_host);

    gap_set_default_link_policy_settings(
        LM_LINK_POLICY_ENABLE_SNIFF_MODE | LM_LINK_POLICY_ENABLE_ROLE_SWITCH);

    hci_set_master_slave_policy(HCI_ROLE_MASTER);

ここでは、classic HID ホストの初期化と、 コールバックの登録を行なっています。

周辺の classic デバイス有無のスキャン

M5stack を device と接続するには、 接続先デバイスの BD_ADDR が必要です。

この BD_ADDR を取得するために、周辺デバイスのスキャンを行ないます。

なお、事前に接続先デバイスの BD_ADDR が分かっている場合、 スキャンは不要です。

このスキャンのサンプルは、以下にあります。

btstack/port/esp32/example/gap_inquiry/main/

指定の classic デバイスへのペアリング

接続先デバイスの BD_ADDR を以下の関数に渡すことで、 そのデバイスと接続が行なわれます。

hid_host_connect( addr, s_hid_host_report_mode, &s_hid_info_in.cid);

なお、ペアリングが成功すると、 ペアリング情報が btstack ライブラリによって FLASH ROM の NVS 上に記録されます。

この情報によって、次回からキーボード側からの接続要求を受けることが可能になります。

ペアリング済みの classic デバイスからの接続要求許可

前述の通り、一度ペアリングすることで、 そのペアリング情報が NVS に記録され、 キーボード側からの接続要求を受けることが可能になります。

この接続要求を受けると、 HID_SUBEVENT_INCOMING_CONNECTION イベントのコールバック関数が呼び出されます。

このコールバック関数で以下を実行することで、 接続要求が受け入れられて接続が確立します。

hid_host_accept_connection( cid, s_hid_host_report_mode );

classic の device

M5stack を Bluetooth keyboard として動作させる場合、 classic の device 制御が必要になります。

btstack の classic device のサンプルプログラムは以下にあります。

btstack/port/esp32/example/hid_keyboard_demo

このサンプルの中の主な処理は以下です。

  • classic HID Device の初期化
  • discoverable の設定
  • report の送信要求
  • report の送信
  • ペアリング後の接続

各処理を以降で説明します。

classic HID Device の初期化

ここでは、classic HID device の初期化と、 コールバックの登録を行なっています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    gap_set_class_of_device( bt_kb_getCod() );
    gap_set_local_name( s_hid_device_name );
    
    gap_set_default_link_policy_settings(
        LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE );
    gap_set_allow_role_switch(true);

    hid_create_sdp_record(s_hid_service_buffer, 0x10001, &hid_params);

    sdp_register_service(s_hid_service_buffer);

    device_id_create_sdp_record(
        s_device_id_sdp_service_buffer, 0x10003,
        DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH,
        BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);

    sdp_register_service(s_device_id_sdp_service_buffer);

    hid_device_init(hid_boot_device, hidDescriptorLen, pHidDescriptor );

    s_hci_event_callback_registration.callback = &packet_handler;
    hci_add_event_handler(&s_hci_event_callback_registration);

    hid_device_register_packet_handler(&packet_handler_device);

discoverable の設定

PC 側のスキャンでリストされるように、 discoverable を有効にします。

gap_discoverable_control( 1 );

この状態で、 PC 側からペアリング処理を行なうと、ペアリングが完了します。

report の送信要求

ペアリングが完了しても、 キーの押下情報を送るにはライブラリ側の準備が必要です。

その送信準備を要求するのが、以下の関数です。

hid_device_request_can_send_now_event( s_hid_info_out.cid );

report の送信

hid_device_request_can_send_now_event() の送信要求によって、 送信準備が行なわれ、準備が整うと HID_SUBEVENT_CAN_SEND_NOW イベントの コールバックが呼ばれます。 このコールバックで以下を実行することで、レポートが送信されます。

hid_device_send_interrupt_message( cid, hidReport, sizeof(hidReport));

前述の送信要求と、送信は非同期に行なわれるので、注意が必要です。

ペアリング後の接続

ペアリング後、再起動時などで devicd 側からホストに接続する場合、以下を実行します。

key_hid_device_connect( addr );

BLE の device

M5stack を BLE keyboard として動作させる場合、 BLE の device 制御が必要になります。

btstack の BLE device のサンプルプログラムは以下にあります。

btstack/port/esp32/example/hog_keyboard_demo

このサンプルの中の主な処理は以下です。

  • BLE HID Device の初期化
  • discoverable の設定
  • report の送信要求
  • report の送信
  • ペアリング後の接続

各処理を以降で説明します。

BLE HID Device の初期化

ここでは、classic HID device の初期化と、 コールバックの登録を行なっています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);

    sm_set_authentication_requirements(
        SM_AUTHREQ_SECURE_CONNECTION | SM_AUTHREQ_BONDING);

    // setup ATT server
    att_server_init(profile_data, NULL, NULL);

    // setup battery service
    battery_service_server_init( 100 );

    // setup device information service
    device_information_service_server_init();

    // setup HID Device service
    hids_device_init(0, get_hidDescriptor(), get_hidDescriptorLen() );

    // setup advertisements
    gap_advertisements_set_params(
        adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
    gap_advertisements_set_data( sizeof(adv_data), (uint8_t*) adv_data);
    
    // register for SM events
    static btstack_packet_callback_registration_t sm_event_callback_registration;
    sm_event_callback_registration.callback = packet_handler;
    sm_add_event_handler(&sm_event_callback_registration);

    // register for HIDS
    hids_device_register_packet_handler(packet_handler);

discoverable の設定

PC 側のスキャンでリストされるように、 discoverable を有効にします。

gap_advertisements_enable(1);

ペアリングの受け入れ

PC 側からペアリング要求が来た際、 そのペアリングを受け入れるかどうかをコールバックで処理します。

発生するイベントは、 BLE を初期化した時の以下の関数の引数によって変わります。

sm_set_io_capabilities();
sm_set_io_capabilities の引数と動作
  • IO_CAPABILITY_DISPLAY_YES_NO

PC 側とデバイス側にコードが表示され、同じコードであるかをユーザが確認し、 問題なければペアリングする。

ペアリング要求を受けると、 SM_EVENT_NUMERIC_COMPARISON_REQUEST イベントが通知されるので、 受け入れるかどうかを処理する。

  • IO_CAPABILITY_KEYBOARD_ONLY

PC 側にコードが表示される。 ユーザがそのコードを M5stack に入力し、 M5stack は、入力されたコードを PC に通知する。 一致すればペアリングする。

PC にコードを通知する際、以下の関数を使用する。

sm_passkey_input()

これをサポートするには、 ユーザが数値を M5stack に入力できるようにする機能が必要になる。

  • IO_CAPABILITY_NO_INPUT_NO_OUTPUT

PC 側とデバイス側にペアリングするかどうかを確認する画面を表示し、 「ペアリングする」が選択された場合に、ペアリングする。

SM_EVENT_JUST_WORKS_REQUEST イベントで処理される。

イベントと処理
  • SM_EVENT_JUST_WORKS_REQUEST イベント

ペアリングを受け入れる場合は以下を実行

sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));

ペアリングを拒否する場合は以下を実行

sm_bonding_decline( sm_event_just_works_request_get_handle(packet) );
  • SM_EVENT_NUMERIC_COMPARISON_REQUEST イベント

PC から通知されたキーが、 PC 側に表示されているキーと等しいかどうかを確認して、 ペアリングを受け入れます。

これは、 device 側にディスプレイがあるケースで利用します。

通知されるキーは、以下で取得できます。

sm_event_numeric_comparison_request_get_passkey(packet)

ペアリングを受け入れる場合は以下を実行

sm_numeric_comparison_confirm( sm_event_numeric_comparison_request_get_handle(packet));

ペアリングを拒否する場合は以下を実行

sm_bonding_decline( sm_event_numeric_comparison_request_get_handle(packet) );
  • case SM_EVENT_PASSKEY_DISPLAY_NUMBER イベント

PC 上に表示された確認用キーを、デバイス上で入力して PC 側に送ります。 これが一致することで、ペアリングを確定します。

これは、ディスプレイがなく、キー入力が可能なケースで利用します。

確認用キーは以下で取得します。

sm_event_passkey_display_number_get_passkey(packet)

送り返す場合は、以下を実行します。

sm_passkey_input()

report の送信要求

ペアリングが完了しても、 キーの押下情報を送るにはライブラリ側の準備が必要です。

その送信準備を要求するのが、以下の関数です。

hids_device_request_can_send_now_event( s_con_handle );

report の送信

前述の送信要求によって、 送信準備が行なわれ、準備が整うと HIDS_SUBEVENT_CAN_SEND_NOW イベントの コールバックが呼ばれます。 このコールバックで以下を実行することで、レポートが送信されます。

hids_device_send_boot_keyboard_input_report()

あるいは

hids_device_send_input_report() 

前述の送信要求と、送信は非同期に行なわれるので、注意が必要です。

ペアリング後の接続

ペアリング後、再起動時などで devicd 側からホストに接続する場合、以下を実行します。

gap_advertisements_enable(1);

以上。