文書の過去の版を表示しています。
目次
BLEキーボードをつなごう(btstack編)
概要
Raspberry Pi Pico W/Pico 2Wや、 ESP32のようなマイクロコントローラを BLEセントラル(== BLEクライアント)として動作させ、BLEペリフェラル(== BLEサーバー)であるBLEキーボードを接続して、キー入力に使用するというサンプルは意外に少ない。 大体が、Raspberry Pi Pico W/Pico 2Wに何らかのスイッチをつないで、それを BLEキーボードとしてPCなどに接続するという話である。 勿論、これらマイクロコントローラが、電子工作の核になる部品であり、PCの周辺機器や、センサーをコントロールするためのものなのだから当然である。
とはいえ、昨今では、SPI接続のビットマップ液晶が安価に購入できるようになっており、これらマイクロコントローラを使ったゲームコンソールや、エミュレータなどの用途がないわけではない。 こういった用途を考えた場合に、必要となるのは、入力デバイスであるキーボードである。
OTGアダプタを経由してUSBキーボードを接続するという方法もあるが、せっかく小型のコントローラである。 BLEキーボードを使用して、余計なケーブルを必要としないスタイルがよりよいであろう。
ここでは、Raspberry Pi Pico W/Pico 2Wにpico-sdkに含まれる btstack を使用して、BLEキーボードを接続し、キー入力を得る方法について、コードを交えながら解説する。 なお、ここに記すことは、わたしの独自の調査に基づく情報を含んでいるため、間違っている可能性があり、この情報に基づいて実装したコードによって生じうるいかなる損害も、わたしは一切関知しないし責任も負わない。 とはいえ、実際に動作しているコードを使用しての解説であるため、少なくともキー入力を得ることができることについては間違いはない。
前提
開発環境として、Visual Studio上のPlatformIOを使用し、フレームワークには Arduinoを使用している。 つまりは、ユーザ側プログラムには main()はなく、setup()と loop()をエントリポイントとして使用している。
pico-sdkおよび、それに含まれる btstack 1.6.2の利用を前提として、ターゲットは Raspberry Pi Pico W(RP2040)および Raspberry Pi Pico 2W (RP2350)である。
言語は C++を使用する。
処理の流れ
BLEは、Bluetooth Low Energyのことであり、従来の Bluetoothより省電力で通信できるプロトコルである。 GATT1)とよばれる属性プロファイルを使用して、データのやり取りを行う。 まずは、BLEスタックを初期化し、HCI2)を起動する。
次に、BLEセントラル側は BLEペリフェラルによる Advertisement をスキャンして、接続したいデバイスとの間にコネクションを張る。 デバイスがなんであるのか、大まかな割り振りはUUIDで分類できる。 キーボード、ゲームパッド、およびマウスというような入力デバイスは、HIDに分類され、UUID16 == 0x1812を持つと決められているので、このUUIDをもつデバイスと接続すればよい。 一般的には、UUID16 == 0x1812であればとりあえず接続してしまうので、キーボードではないかもしれないが、キーボードやマウスの側で、明示的に接続モードに入っていない限りは Advertiseは行われないので、間違ったデバイスに接続することは稀有だろう。 そういう心配がある場合には、デバイス名などの情報を取得し、それに接続してもいいかというような選択肢を出すことも考えるべきだろうが、ここでは割愛する。
この時、デバイスによっては接続を暗号化してセキュアにする必要がある場合がある。 キーボードの場合は、その性質上打鍵情報を傍受される危険性があるためか、セキュアであることを要求しているものが少なくない。 手元にあるキーボードでは、バッファロー製のものと、Capdputerを購入したときに入っているサンプルのBLEキーボード化アプリがセキュア接続を要求しない3)が、他はすべてセキュア接続を要求する。 この要件は、キー入力を通知する Characteristic の属性に記されており、セキュア接続を必須とする場合に、セキュアでない接続上でキー入力のデータを要求しても要求が失敗する。
接続が確立したら、Characteristicsを取得する。 一つのデバイスは複数の Characteristicsを持ちうるので、デバイスに問い合わせるのだが、この時取得は一つずつ得て、終わるとCOMPETEのイベントが発生する。
全ての Characteristicsが取得され、GATT_EVENT_QUERY_COMPETEのイベントが発生したら、今度は個々のCharacteristicを調べ、Notificationをサポートしてるものを探す。 このため、上記で得られた Characteristicsの一覧をどこかに保持しておく必要がある。 ここでは std::list<gatt_client_characteristic_t>を用意して、そこに格納しておくことにする。
このリストを捜査しながら、Notification可能な Characteristicに対して、Descriptor一覧を要求し、中に含まれる CCCD4)を探し、0x00015)を書き込む。 この時の操作は、一つの Characteristicに対して、Descriptor一覧の要求を行い、CCCDを探して 0x0001を書き込み、この返事が来るまでは、他の Characteristicに対しての処理を行うことができない。
これは、処理がBLEセントラルとBLEペリフェラルの双方にまたがっており、一つの要求に対する一つの返事が完了するまで次の操作に移れないためである。 このことがコードを読みにくくし、処理を追いにくくしているが、デバイスをまたがったループの構築を行う必要があるということである。
これを実現するために、一般的にはループローカルで作成する std::list<gatt_client_characteristic_t>::iterator をグローバルに用意し、このイテレータで、現在どの Characteristicに対する処理が行われていて、すべてに対する処理が終わったかどうかを管理する。
全ての必要なCCCDに対して0x0001の書き込みが済んだら、Notificationを取得した際に呼び出されるコールバックを登録して、後はそちらでキー入力を待つ処理を行う。
実際の処理
初期化
Raspberry Pi Pico W/2Wについては、ファームウェアに cyw43関連の物理層が用意されており、board = rpipicow または rpipico2w としてビルドすると、自動的に組み込まれ初期化される。 16KBほどの領域がこのことにより消費されるが、その代わり hci_init()や、btstack_run_loop()などの一部の初期化やループからの呼び出し処理を必要としない。
static btstack_packet_callback_registration_t hci_event_callback_registration; static btstack_packet_callback_registration_t sm_event_callback_registration; void setup() { Serial.begin(115200); btstack_memory_init(); l2cap_init(); gatt_client_init(); sm_init(); btstack_crypto_init(); // security manager sm_set_request_security(true); sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION); sm_set_encryption_key_size_range(7, 16); // register event handlers hci_event_callback_registration.callback = &hci_event_handler; hci_add_event_handler(&hci_event_callback_registration); sm_even_callback_registration.callback = &sm_event_handler; sm_add_event_handler(&sm_event_callback_registration); // turn on HCI hci_power_control(HCI_POWER_ON); }
ここでは、HCIやSecurity Managerといった必要なモジュールを初期化して、HCIを起動する。 sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT)とsm_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION)との組み合わせは、パスキーなどの交換をしないで、暗号化キーを直接交換するJust Worksでの暗号化を要求する組み合わせである。 パスキーなどを表示したり交換したりする場合には別の組み合わせと適切なイベントハンドラの実装が必要となるがここでは割愛する。
接続処理
HCIの動作開始確認
HCIをONにすると、登録した hci_event_handler()にBTSTACK_STATE_EVENTパケットが、state==HCI_STATE_WORKINGで渡ってくる。 これがHCIが動作開始した合図になるので、ここで、Advertising をスキャン開始する。
gap_set_scan_parameter(0, 0x30, 0x30) はAdvertisingをスキャンするパラメータの設定である。 引数は scan type, interval, window になるが、サンプルからの引き写しである。
gap_start_scan()をかけると Advertisingのスキャンが開始される。
Advertisingのスキャン
スキャンが開始され、Advertiseしているデバイスがみつかると、hci_event_handler()に GAP_EVENT_ADVERTISING_REPORT パケットが渡される。 デバイスのアドレス、タイプ、rssi やデータ長などの基本データはこのハンドラの中で取り出している。 btstackにおけるイベントハンドラーでは、パケットからデータを取り出すための手続きが用意されているのでそれを使用する。 原則的に、<イベント名>_get_<取り出したいデータ名>という名前になっている。 Visual Studio Code を使用ていて、Intellisenseが動いているのであれば、<イベント名>_get_ と入力すれば、存在する名前がずらっと出てくるし、おそらく引数とかもサジェストされるので、いちいち調べないでもコーディングは可能だと思われる。
なお、サンプルによっては、関係ないイベントのスクレイパーを呼び出していたりするが、それはたまたまうまく動いているにすぎないので、正しいものを使用するようにしたい。 勿論、わたしの書いているサンプルにも、コードの切り貼りなどをしている過程で不適切なものが混入したままになっている可能性は大いにあるため、コピペなどで適当に済ませずに、検証して使用されたい。
Advertise情報は、パケット中に配列の形で埋め込まれているので、scan_advertisements()関数でその部分を処理している。
キーボードなのでHIDデバイス(UUID16 == 0x1812)のみをターゲットとして受信するために、フィルタリングをここで行っている。 勿論、マウスやゲームパッドも 0x1812なので、本来はもっときちんと検証してキーボードだけを確実に捕まえるようにするべきなのだが、ハイハイスクールアドベンチャーやフルーツフィールドといったゲームなどの場合はそこまで厳密にやらないでも、無関係なHIDデバイスを捕まえてしまうリスクは少ないと考えているのでサボっている。
Advertise情報は、先頭にデータ長が 1byteで、次にデータのタイプが 1byteで、その後ろにデータ長分のデータが並んでいる格好になる。 UUIDは BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS または BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS で渡されるので、いずれかがあった場合にUUIDを取り出している。
BLEのパケットはリトルエンディアンでデータが並ぶ原則になっているので、読み出しもそのように指定して読みだしている。
BLUETOOTH_DATA_TYPE_SHORTENED_LOCAL_NAMEまたは BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAMEをチェックしているのは、M5-Keyboard というデバイス名でやってくる Cardputer のデモプログラムのBLEキーボードがセキュア接続をサポートしていないため、これをセキュア接続の対象から除外するための処理である。
本来は、これも、Characteristics をチェックして、Notificationを要求するのにセキュア接続が必要かどうかを見た上で、セキュア接続するかどうかを判別するべきで、デバイス名でやるべきではないのだが、実用上これでも問題がないので、現在はこの方法で逃げている。
実際大半のキーボードはセキュア接続をサポートしている6)ので、今後対象が増えるとも思いにくいのでよしとしておく。
キーボード(HIDデバイス)が見つかったら、gap_stop_scan()でAdvertisingのスキャンを停止し、gap_connect()で接続を行う。
接続完了とセキュア接続の開始
デバイスと接続すると、HCI_EVENT_LE_META イベントが state == HCI_SUBEVENT_LE_CONNECTION_COMPLETE で渡される。 所々フランス語が混ざっているのが何とも言えない。
コネクションが確立したところで、セキュア接続を開始する。 7)
sm_request_pairing(connection_handle)がセキュア接続要求の開始で、この処理以降は、sm_event_handler()に処理が移る。
static bd_addr_t keyboard_address; static int found_hid_device = 0; // 広告パケットを解析してHID UUIDを確認 static void scan_advertisements(const uint8_t *packet, uint16_t size) { ad_data_iterator_t ad_iter; uint16_t uuid; // 広告データの反復処理 for (ad_iter.data = packet, ad_iter.size = size, ad_iter.offset = 0 ; ad_iter.offset < ad_iter.size ; ad_data_iterator_next(&ad_iter)) { if (ad_iter.data[ad_iter.offset + 1] == BLUETOOTH_DATA_TYPE_SHORTENED_LOCAL_NAME|| ad_iter.data[ad_iter.offset + 1] == BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME) { // デバイス名を取得 char device_name[32]; memcpy(device_name, &ad_iter.data[ad_iter.offset + 2], ad_iter.data[ad_iter.offset] - 1); device_name[ad_iter.data[ad_iter.offset] - 1] = '\0'; if (strstr(device_name, "M5-Keyboard") != NULL) { secure_connection = false; // セキュアコネクションを無効化 continue; } } // 16ビットUUIDを確認 if (ad_iter.data[ad_iter.offset + 1] == BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS || ad_iter.data[ad_iter.offset + 1] == BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS) { uuid = little_endian_read_16(&ad_iter.data[ad_iter.offset + 2], 0); if (uuid == HID_SERVICE_UUID) { found_hid_device = 1; continue; } } } } // HCIイベントハンドラ static void hci_event_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { if (packet_type != HCI_EVENT_PACKET) return; switch (hci_event_packet_get_type(packet)) { case BTSTACK_EVENT_STATE: if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) { gap_set_scan_parameters(0, 0x0030, 0x0030); gap_start_scan(); } break; case GAP_EVENT_ADVERTISING_REPORT: { gap_event_advertising_report_get_address(packet, keyboard_address); bd_addr_type_t address_type = static_cast<bd_addr_type_t>(gap_event_advertising_report_get_address_type(packet)); uint8_t rssi = gap_event_advertising_report_get_rssi(packet); uint8_t data_length = gap_event_advertising_report_get_data_length(packet); scan_advertisements(gap_event_advertising_report_get_data(packet), gap_event_advertising_report_get_data_length(packet)); if (found_hid_device) { gap_stop_scan(); gap_connect(keyboard_address, address_type); found_hid_device = 0; // リセット } break; } case HCI_EVENT_DISCONNECTION_COMPLETE: gap_start_scan(); // 再スキャン開始 break; case HCI_EVENT_LE_META: if (hci_event_le_meta_get_subevent_code(packet) == HCI_SUBEVENT_LE_CONNECTION_COMPLETE) { // 暗号化を有効化 hci_con_handle_t connection_handle = gap_subevent_le_connection_complete_get_connection_handle(packet); gap_request_security_level(connection_handle, LEVEL_2); if (secure_connection) { // セキュアコネクションを有効化 sm_request_pairing(connection_handle); } else { // get primary service gatt_client_discover_primary_services_by_uuid16(&handle_gatt_client_event, connection_handle, HID_SERVICE_UUID); // GATT event handler } } break; case HCI_EVENT_ENCRYPTION_CHANGE: if (!hci_event_encryption_change_get_encryption_enabled(packet)) { Serial.printf("Encryption failed.\n"); } break; default: break; } }
セキュア化
セキュア接続開始
sm_request_pairing()がデバイス側に届くと、SM_EVENT_PAIRING_STARTED イベントが発生する。 これはセントラル側で特に行うことはない。 サンプルではただアドレスを表示したりしている。
セキュア接続リクエストの処理
これがセキュア接続の肝になる部分である。 初期化時に行った、sm_set_io_capabilities()とsm_set_authentication_requirements()の組み合わせによって、パスキーを受け取ったり、渡したり、受け取ったキーを表示して、Yes/Noを受け取ったりという処理が必要になる場合があるが、ここでは Just Worksになるように組み合わせているので、SM_EVENT_JUST_WORKS_REQUEST が発生する。
Just Worksなので、自動的に承認を返すだけでよい。 sm_just_works_confirm(sm_event_just_works_request_get_handle(packet)); を呼び出せばデバイス側に承認が伝わる。
sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); // IOキャパビリティ設定 sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION); // 認証要件を設定
セキュア接続の完了とプライマリサービスの取得要求
デバイスにセキュア接続の承認が伝わると、SM_EVENT_PAIRING_COMPLETEが発生するので、gatt_client_discover_primary_services_by_uuid16()を呼び出し、処理をGATTの処理に移行する。 この関数で、コールバックを handle_gatt_client_event()に切り替える。
// SMイベントハンドラ static void sm_event_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { if (packet_type != HCI_EVENT_PACKET) return; switch (hci_event_packet_get_type(packet)) { case SM_EVENT_JUST_WORKS_REQUEST: { // Just Works Confirmを自動的に承認 sm_just_works_confirm(sm_event_just_works_request_get_handle(packet)); break; } case SM_EVENT_PAIRING_STARTED: break; case SM_EVENT_PAIRING_COMPLETE: { uint8_t status = sm_event_pairing_complete_get_status(packet); if (status == ERROR_CODE_SUCCESS) { hci_con_handle_t connection_handle = sm_event_pairing_complete_get_handle(packet); gatt_client_discover_primary_services_by_uuid16(&handle_gatt_client_event, connection_handle, HID_SERVICE_UUID); // GATT event handler } else { Serial.printf("Pairing failed with status %u.\n", status); } break; } case SM_EVENT_REENCRYPTION_STARTED: { // 再暗号化が開始された場合の処理 bd_addr_t addr; sm_event_reencryption_started_get_address(packet, addr); // アドレスを取得 uint8_t addr_type = sm_event_reencryption_started_get_addr_type(packet); break; } case SM_EVENT_REENCRYPTION_COMPLETE: { // 再暗号化が完了した場合の処理 uint8_t status = sm_event_reencryption_complete_get_status(packet); if(status != ERROR_CODE_SUCCESS) { Serial.printf("Re-encryption failed.(%d)\n", status); } break; } default: break; } }
プライマリサービスの取得
サービスハンドルの取得
gatt_client_discover_primary_services_by_uuid16()がデバイス側に受理されると、GATT_EVENT_SERVICE_QUERY_RESULTが発生する。
BLEデバイスについては、一つのデバイスであっても、複数のサービスを持っているのが普通なので、HID_SERVICE_UUID(0x1812)のサービスだけを扱うようにフィルタリングしている。
勿論、HIDサービス以外を処理できるように、コードを追加することもできる。
HIDサービスのCharacteristics取得要求
GATTのリクエストが完了すると、これも共有の GATT_EVENT_QUERY_COMPLETE が発生する。 このイベントは、次の要求を発呼できる状態になったという合図なので、これを待ってから次の要求を行うのが作法である。
AIなどに聞くと、前段の REPORTイベントの中でガンガン要求を発呼していたりするので、動かなかったりする。
またGATTにおいて、デバイスに何かを要求すると、その完了を伝えるためにこのイベントが発生する。 つまり、さまざまな要求がこのイベントを共有するので、今何をリクエストしているのかを覚えていなければならない。 btstack のサンプルなどでは state というグローバル変数を用意して、何を処理中なのかをここで管理するステートマシンを作っている。
それはそれで、おそらくスマートなんだろうけれど、イベントハンドラの中が state による場合分けで読みにくくなりそうなので、要求ごとにハンドラを分ける形をとっている。
なので、handle_gatt_client_event()ハンドラで処理するのは、gatt_client_discover_primary_services_by_uuid16()にかかわるCOMPLETEだけになっている。
HIDに対するサービスハンドルhid_serviceは取得済み8)なので、これに対して Characteristicsの取得を要求する。 この要求に対する処理は handle_gatt_characteristics_discovered()に渡される。
static gatt_client_service_t hid_service; // GATT関連のイベントの入り口 static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { switch(hci_event_packet_get_type(packet)) { case GATT_EVENT_SERVICE_QUERY_RESULT: { // サービスのUUIDを取得 gatt_client_service_t service; gatt_event_service_query_result_get_service(packet, &service); hci_con_handle_t connection_handle = gatt_event_service_query_result_get_handle(packet); uint16_t uuid16 = service.uuid16; const uint8_t *uuid128 = service.uuid128; if (uuid16 == 0) { uuid16 = little_endian_read_16(uuid128, 0); } if (uuid16 == HID_SERVICE_UUID) { // characteristicを探索するのはGATT_EVENT_QUERY_COMPLETEで行う必要がある。 hid_service = service; // HIDサービスを保存 } } break; case GATT_EVENT_QUERY_COMPLETE: { uint16_t status = gatt_event_query_complete_get_att_status(packet); // Replace with the correct function if (status != 0) { Serial.printf("GATT query failed with status %u\n", status); } else { if (hid_characteristics_scan_completed) break; // HID Reportの探索が完了している場合はスキップ // GATTクライアントのサービスを探索する hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); // Retrieve connection handle gatt_client_discover_characteristics_for_service(&handle_gatt_characteristics_discovered, connection_handle, &hid_service); // GATT event handler } } break; default: break; } }
Characteristicsの取得からCCCDに対する要求
Characteristics一覧の取得と保存
gatt_client_discover_characteristics_for_service()がデバイスに処理されると、GATT_EVENT_CHARACTERISTIC_QUERY_RESULTのイベントが発生する。
一般的にBLEデバイスでは、サービスごとに複数のCharacteristicsが紐づいている。
なので、一つの gatt_client_discover_characteristics_for_service()に対して、複数の GATT_EVENT_CHARACTERISTIC_QUERY_RESULTが渡される。
その際にひとつの characteristicが渡されるので、これを hid_characteristicsに保存しておく。 なお、キー入力を受け取るには HID_REPORT_DATA 9)だけを見ればいいので、ここでフィルタリングしてしまってもいいのかもしれない。
このフィルタリングは後で Descriptorをチェックしたり、Notificationを要求したりする段で行っている。
Notificationの要求処理開始
全ての Characteristics を受取り終わると、GATT_EVENT_QUERY_COMPLETEが発生する。 この先は、渡された Characteristics を走査して、HID_REPORT_DATAに関連する Characteristicで Notification可能なものがあれば、それに対して Notificationを送るように要求していく処理になる。
ここで、BLEというかGATTの面倒くさい部分になるのだが、Characteristicひとつに対して要求の処理を行うと、それが終わるまで次の Characteristicに対する要求に移れない。
何を言っているのかというと、感覚的に、次のようなループで処理をすすめたくなるが、これではうまく動かないのだ。
for(auto it = hid_characteristics.begin(); it != hid_characteristics.end(); ++it) { // HID ReportのUUIDを確認 if (it->uuid16 == 0x2a4d) { // UUIDがHID_REPORT_DATAの場合 uint16_t characteristic_handle = it->value_handle; uint8_t properties = it->properties; if (properties & ATT_PROPERTY_NOTIFY) { // Notificationをサポートしているか確認 hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); gatt_client_discover_characteristic_descriptors(&handle_gatt_descriptors_discovered, connection_handle, &*it); } } }
最初に、Notification可能な Characteristicを見つけて、gatt_client_discover_characteristic_descriptors()に要求した時点で、このループの後続のリクエストは10)無視される。
これを回避するためにループ制御をローカルのiteratorではなくて、hid_characteristic_itをグローバルに用意して、中断した場所を覚えておいて、最初のリクエストが完了した時点で次に移れるようにしておく。
gatt_client_discover_characteristic_descriptors()の戻りについてはhandle_gatt_descriptors_discovered()に処理が移る。
Notificationの要求について
デバイスに対して Notificationを要求するには、Characteristicの Descriptorsの中からCCCD11)を見つけて、それに対してNotificationの要求 0x0001を書き込む必要がある。
このため、Characteristicの Descriptor一覧を取得する必要がある。
static std::list<gatt_client_characteristic_t> hid_characteristics; static std::list<gatt_client_characteristic_t>::iterator hid_characteristic_it; // Characteristicを見つけた。 static void handle_gatt_characteristics_discovered(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { if (packet_type != HCI_EVENT_PACKET) return; switch (hci_event_packet_get_type(packet)) { case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT: { gatt_client_characteristic_t characteristic; gatt_event_characteristic_query_result_get_characteristic(packet, &characteristic); uint16_t characteristic_handle = characteristic.value_handle; uint8_t properties = characteristic.properties; hid_characteristics.push_back(characteristic); // HID Reportを保存 } break; case GATT_EVENT_QUERY_COMPLETE: { uint16_t status = gatt_event_query_complete_get_att_status(packet); // Replace with the correct function if (status != 0) { Serial.printf("GATT Characteristcis query failed with status %u\n", status); } else { for(hid_characteristic_it = hid_characteristics.begin(); hid_characteristic_it != hid_characteristics.end(); ++hid_characteristic_it) { // HID ReportのUUIDを確認 if (hid_characteristic_it->uuid16 == 0x2a4d) { // UUIDがHID_REPORT_DATAの場合 uint16_t characteristic_handle = hid_characteristic_it->value_handle; uint8_t properties = hid_characteristic_it->properties; if (properties & ATT_PROPERTY_NOTIFY) { // Notificationをサポートしているか確認 hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); gatt_client_discover_characteristic_descriptors(&handle_gatt_descriptors_discovered, connection_handle, &*hid_characteristic_it++); break; } } } } } break; default: break; } }