bleキーボードをつなごう_btstack編
差分
このページの2つのバージョン間の差分を表示します。
| 次のリビジョン | 前のリビジョン | ||
| bleキーボードをつなごう_btstack編 [2025/04/22 00:03] – 作成 araki | bleキーボードをつなごう_btstack編 [2025/04/22 06:13] (現在) – [前提] araki | ||
|---|---|---|---|
| 行 26: | 行 26: | ||
| 言語は C++を使用する。 | 言語は C++を使用する。 | ||
| + | なお、この文書やプログラムの作成に当たっては[[https:// | ||
| + | |||
| + | また、Copilot、Gemini、およびLM Studio上の Gemma3 12BなどAIによる支援も利用した。 | ||
| + | |||
| + | 完全なコードは[[https:// | ||
| ===== 処理の流れ ===== | ===== 処理の流れ ===== | ||
| 行 100: | 行 105: | ||
| ==== 接続処理 ==== | ==== 接続処理 ==== | ||
| + | |||
| + | === HCIの動作開始確認 === | ||
| + | |||
| + | HCIをONにすると、登録した hci_event_handler()にBTSTACK_STATE_EVENTパケットが、state==HCI_STATE_WORKINGで渡ってくる。 | ||
| + | これがHCIが動作開始した合図になるので、ここで、Advertising をスキャン開始する。 | ||
| + | |||
| + | gap_set_scan_parameter(0, | ||
| + | 引数は scan type, interval, window になるが、サンプルからの引き写しである。 | ||
| + | |||
| + | gap_start_scan()をかけると Advertisingのスキャンが開始される。 | ||
| + | |||
| + | === Advertisingのスキャン === | ||
| + | |||
| + | スキャンが開始され、Advertiseしているデバイスがみつかると、hci_event_handler()に GAP_EVENT_ADVERTISING_REPORT パケットが渡される。 | ||
| + | デバイスのアドレス、タイプ、rssi やデータ長などの基本データはこのハンドラの中で取り出している。 | ||
| + | btstackにおけるイベントハンドラーでは、パケットからデータを取り出すための手続きが用意されているのでそれを使用する。 | ||
| + | 原則的に、< | ||
| + | Visual Studio Code を使用ていて、Intellisenseが動いているのであれば、< | ||
| + | |||
| + | なお、サンプルによっては、関係ないイベントのスクレイパーを呼び出していたりするが、それはたまたまうまく動いているにすぎないので、正しいものを使用するようにしたい。 | ||
| + | 勿論、わたしの書いているサンプルにも、コードの切り貼りなどをしている過程で不適切なものが混入したままになっている可能性は大いにあるため、コピペなどで適当に済ませずに、検証して使用されたい。 | ||
| + | |||
| + | 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を要求するのにセキュア接続が必要かどうかを見た上で、セキュア接続するかどうかを判別するべきで、デバイス名でやるべきではないのだが、実用上これでも問題がないので、現在はこの方法で逃げている。 | ||
| + | |||
| + | 実際大半のキーボードはセキュア接続をサポートしている((キーボードというデバイスの性質上当然だろう。))ので、今後対象が増えるとも思いにくいのでよしとしておく。 | ||
| + | |||
| + | キーボード(HIDデバイス)が見つかったら、gap_stop_scan()でAdvertisingのスキャンを停止し、gap_connect()で接続を行う。 | ||
| + | |||
| + | === 接続完了とセキュア接続の開始 === | ||
| + | |||
| + | デバイスと接続すると、HCI_EVENT_LE_META イベントが state == HCI_SUBEVENT_LE_CONNECTION_COMPLETE で渡される。 | ||
| + | 所々フランス語が混ざっているのが何とも言えない。 | ||
| + | |||
| + | コネクションが確立したところで、セキュア接続を開始する。 | ||
| + | ((但し、M5-Keyboard の場合には、セキュア接続の処理を飛ばして、プライマリサービスの取得要求を行う。)) | ||
| + | |||
| + | sm_request_pairing(connection_handle)がセキュア接続要求の開始で、この処理以降は、sm_event_handler()に処理が移る。 | ||
| <code cpp> | <code cpp> | ||
| + | static bd_addr_t keyboard_address; | ||
| + | static int found_hid_device = 0; | ||
| + | |||
| // 広告パケットを解析してHID UUIDを確認 | // 広告パケットを解析してHID UUIDを確認 | ||
| static void | static void | ||
| 行 148: | 行 205: | ||
| case BTSTACK_EVENT_STATE: | case BTSTACK_EVENT_STATE: | ||
| if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) { | if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) { | ||
| - | bd_addr_t local_address; | ||
| - | gap_local_bd_addr(local_address); | ||
| gap_set_scan_parameters(0, | gap_set_scan_parameters(0, | ||
| gap_start_scan(); | gap_start_scan(); | ||
| 行 203: | 行 258: | ||
| ==== セキュア化 ==== | ==== セキュア化 ==== | ||
| + | |||
| + | === セキュア接続開始 === | ||
| + | |||
| + | sm_request_pairing()がデバイス側に届くと、SM_EVENT_PAIRING_STARTED イベントが発生する。 | ||
| + | これはセントラル側で特に行うことはない。 | ||
| + | サンプルではただアドレスを表示したりしている。 | ||
| + | |||
| + | === セキュア接続リクエストの処理 === | ||
| + | |||
| + | これがセキュア接続の肝になる部分である。 | ||
| + | 初期化時に行った、sm_set_io_capabilities()とsm_set_authentication_requirements()の組み合わせによって、パスキーを受け取ったり、渡したり、受け取ったキーを表示して、Yes/ | ||
| + | |||
| + | Just Worksなので、自動的に承認を返すだけでよい。 | ||
| + | sm_just_works_confirm(sm_event_just_works_request_get_handle(packet)); | ||
| + | |||
| + | |||
| + | <code cpp> | ||
| + | sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); | ||
| + | sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION); | ||
| + | </ | ||
| + | |||
| + | === セキュア接続の完了とプライマリサービスの取得要求 === | ||
| + | |||
| + | デバイスにセキュア接続の承認が伝わると、SM_EVENT_PAIRING_COMPLETEが発生するので、gatt_client_discover_primary_services_by_uuid16()を呼び出し、処理をGATTの処理に移行する。 | ||
| + | この関数で、コールバックを handle_gatt_client_event()に切り替える。 | ||
| + | |||
| + | <code cpp> | ||
| + | // SMイベントハンドラ | ||
| + | static void | ||
| + | sm_event_handler(uint8_t packet_type, | ||
| + | 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(& | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | Serial.printf(" | ||
| + | } | ||
| + | break; | ||
| + | } | ||
| + | case SM_EVENT_REENCRYPTION_STARTED: | ||
| + | { | ||
| + | // 再暗号化が開始された場合の処理 | ||
| + | bd_addr_t addr; | ||
| + | sm_event_reencryption_started_get_address(packet, | ||
| + | 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(" | ||
| + | } | ||
| + | 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は取得済み((であるはず))なので、これに対して Characteristicsの取得を要求する。 | ||
| + | この要求に対する処理は handle_gatt_characteristics_discovered()に渡される。 | ||
| + | |||
| + | <code cpp> | ||
| + | static gatt_client_service_t hid_service; | ||
| + | |||
| + | // GATT関連のイベントの入り口 | ||
| + | static void | ||
| + | handle_gatt_client_event(uint8_t packet_type, | ||
| + | { | ||
| + | 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, | ||
| + | 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, | ||
| + | } | ||
| + | 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); | ||
| + | if (status != 0) { | ||
| + | Serial.printf(" | ||
| + | } else { | ||
| + | if (hid_characteristics_scan_completed) break; // HID Reportの探索が完了している場合はスキップ | ||
| + | // GATTクライアントのサービスを探索する | ||
| + | hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); | ||
| + | gatt_client_discover_characteristics_for_service(& | ||
| + | } | ||
| + | } | ||
| + | break; | ||
| + | default: | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| ==== Characteristicsの取得からCCCDに対する要求 ==== | ==== Characteristicsの取得からCCCDに対する要求 ==== | ||
| - | ==== Notificationの処理準備 ==== | + | === 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 ((0x2a4d))だけを見ればいいので、ここでフィルタリングしてしまってもいいのかもしれない。 | ||
| + | |||
| + | このフィルタリングは後で Descriptorをチェックしたり、Notificationを要求したりする段で行っている。 | ||
| + | |||
| + | === Notificationの要求処理開始 === | ||
| + | |||
| + | 全ての Characteristics を受取り終わると、GATT_EVENT_QUERY_COMPLETEが発生する。 | ||
| + | この先は、渡された Characteristics を走査して、HID_REPORT_DATAに関連する Characteristicで Notification可能なものがあれば、それに対して Notificationを送るように要求していく処理になる。 | ||
| + | |||
| + | ここで、BLEというかGATTの面倒くさい部分になるのだが、Characteristicひとつに対して要求の処理を行うと、それが終わるまで次の Characteristicに対する要求に移れない。 | ||
| + | |||
| + | 何を言っているのかというと、感覚的に、次のようなループで処理をすすめたくなるが、これではうまく動かないのだ。 | ||
| + | <code cpp> | ||
| + | for(auto it = hid_characteristics.begin(); | ||
| + | { | ||
| + | // HID ReportのUUIDを確認 | ||
| + | if (it-> | ||
| + | uint16_t characteristic_handle = it-> | ||
| + | uint8_t properties = it-> | ||
| + | if (properties & ATT_PROPERTY_NOTIFY) { // Notificationをサポートしているか確認 | ||
| + | hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); | ||
| + | gatt_client_discover_characteristic_descriptors(& | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | 最初に、Notification可能な Characteristicを見つけて、gatt_client_discover_characteristic_descriptors()に要求した時点で、このループの後続のリクエストは((おそらく))無視される。 | ||
| + | |||
| + | これを回避するためにループ制御をローカルのiteratorではなくて、hid_characteristic_itをグローバルに用意して、中断した場所を覚えておいて、最初のリクエストが完了した時点で次に移れるようにしておく。 | ||
| + | |||
| + | gatt_client_discover_characteristic_descriptors()の戻りについてはhandle_gatt_descriptors_discovered()に処理が移る。 | ||
| + | |||
| + | == Notificationの要求について == | ||
| + | |||
| + | デバイスに対して Notificationを要求するには、Characteristicの Descriptorsの中からCCCD((Client Characteristic Configuration Descriptor))を見つけて、それに対してNotificationの要求 0x0001を書き込む必要がある。 | ||
| + | |||
| + | このため、Characteristicの Descriptor一覧を取得する必要がある。 | ||
| + | |||
| + | <code cpp> | ||
| + | static std:: | ||
| + | static std:: | ||
| + | |||
| + | |||
| + | // Characteristicを見つけた。 | ||
| + | static void | ||
| + | handle_gatt_characteristics_discovered(uint8_t packet_type, | ||
| + | { | ||
| + | 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, | ||
| + | uint16_t characteristic_handle = characteristic.value_handle; | ||
| + | uint8_t properties = characteristic.properties; | ||
| + | hid_characteristics.push_back(characteristic); | ||
| + | } | ||
| + | break; | ||
| + | case GATT_EVENT_QUERY_COMPLETE: | ||
| + | { | ||
| + | uint16_t status = gatt_event_query_complete_get_att_status(packet); | ||
| + | if (status != 0) { | ||
| + | Serial.printf(" | ||
| + | } else { | ||
| + | for(hid_characteristic_it = hid_characteristics.begin(); | ||
| + | { | ||
| + | // HID ReportのUUIDを確認 | ||
| + | if (hid_characteristic_it-> | ||
| + | uint16_t characteristic_handle = hid_characteristic_it-> | ||
| + | uint8_t properties = hid_characteristic_it-> | ||
| + | if (properties & ATT_PROPERTY_NOTIFY) { // Notificationをサポートしているか確認 | ||
| + | hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); | ||
| + | gatt_client_discover_characteristic_descriptors(& | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | break; | ||
| + | default: | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | ==== Descriptor一覧の取得とNotificationの要求 ==== | ||
| + | |||
| + | === Descriptorの取得 === | ||
| + | |||
| + | gatt_client_discover_characteristic_descriptors()をデバイスが受け取ると、GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULTが発生する。 | ||
| + | ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT って書いてあるけれど、全部が一気に渡されるわけではなく、GATT_EVENT_CHARACTERISTIC_QUERY_RESULT同様に一つずつ渡ってくる。 | ||
| + | |||
| + | なので、 hid_descriptors に記録しておく。 | ||
| + | 勿論、ここで CCCD ((0x2902))のみをフィルタリングしてもいいし、意識高く、NimBLEのように再利用可能なラッパーライブラリーを目指すなら、characteristicごとに descriptor一覧を保存しておくというのもありだろう。 | ||
| + | |||
| + | ここではCCCDにNotification要求をしたら、あとは特に descriptorを使う用もないので、捨ててしまう前提で組んでいる。 | ||
| + | それでも globalに staticなリストとして保存するのは、これも例によって、一つの要求を出したらそれが COMPLETEするのを待たねばならないためである。 | ||
| + | |||
| + | === CCCDの検索とNotificationの要求 === | ||
| + | |||
| + | 全ての descriptorがわたし終わると、GATT_EVENT_QUERY_COMPLETEが発生する。 | ||
| + | hid_descriptorsを走査して、UUID16 == 0x2902 の descriptorを探し、そこに Notification要求を書き込む。 | ||
| + | |||
| + | notification_enableはバイト型の配列で 0x01, 0x00 の順でデータが書き込まれている。 | ||
| + | GATTのデータは little endian でやり取りされるので、この順で書き込む必要がある。 | ||
| + | 勿論、手元の処理系が little endianである場合には普通に 0x0001の uint16_tデータを渡しても構わないが、汎用化するためにこのようにしてある。 | ||
| + | |||
| + | gatt_client_write_value_of_characteristic()には handle_gatt_noification_activated()をイベントハンドラとして登録し、そちらで完了の処理を行う。 | ||
| + | |||
| + | == 処理の続きの部分について == | ||
| + | |||
| + | ここまでは、要求を出したら原則処理はそこで終わりだったが、ここでは続きの部分がある。 | ||
| + | Descriptorを最後まで捜査して、CCCDが見つからなかった場合には、次のCharacteristicに対して gatt_client_discover_characteristic_descriptors()を要求しなければならない。 | ||
| + | なので、hid_descriptorsをクリアして、再取得に備える。 | ||
| + | |||
| + | hid_characteristics_it が hid_characteristeics.end()に到達していたら、もう必要な処理はないので、おしまいである。 | ||
| + | |||
| + | === CCCDに対する要求完了 === | ||
| + | |||
| + | CCCDに対する要求が完了したら GATT_EVENT_QUERY_COMPLETEが発生する。 | ||
| + | 中断したところから次の CCCDを探し、終端まで行ったら次の Characteristicに対して、gatt_client_discover_characteristic_descriptors()をかける。 | ||
| + | |||
| + | そして、characteristicsも終端まで行ったらおしまいである。 | ||
| + | |||
| + | === Notificationの処理登録 === | ||
| + | |||
| + | 処理が終端に到達したら、BLEセントラル側で Notificationの受け取り準備をする。 | ||
| + | |||
| + | <code cpp> | ||
| + | gatt_client_listen_for_characteristic_value_updates(& | ||
| + | </ | ||
| + | |||
| + | Characteristics一つ一つに対して処理を要求することもできるようだが、ここでは一括で行う。 | ||
| + | 最後の nullptrが、全部の Characteristicsに対しての要求を意味している。 | ||
| + | |||
| + | これであとは notification_handler でGATT_EVENT_NOTIFICATIONのイベントを処理すれば目的達成である。 | ||
| + | |||
| + | <code cpp> | ||
| + | static std:: | ||
| + | static std:: | ||
| + | static std:: | ||
| + | static std:: | ||
| + | |||
| + | static uint8_t notification_enable[] = {0x01, 0x00}; // 通知を有効化する値 | ||
| + | static gatt_client_notification_t notification_listener; | ||
| + | |||
| + | // CCCDにNotificationを要求し完了した | ||
| + | static void | ||
| + | handle_gatt_noification_activated(uint8_t packet_type, | ||
| + | { | ||
| + | if (packet_type != HCI_EVENT_PACKET) return; | ||
| + | switch (hci_event_packet_get_type(packet)) | ||
| + | { | ||
| + | case GATT_EVENT_QUERY_COMPLETE: | ||
| + | { | ||
| + | uint16_t status = gatt_event_query_complete_get_att_status(packet); | ||
| + | if (status != 0) { | ||
| + | Serial.printf(" | ||
| + | } else { | ||
| + | uint16_t service_id = gatt_event_query_complete_get_service_id(packet); | ||
| + | uint16_t handle = gatt_event_query_complete_get_handle(packet); | ||
| + | hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); | ||
| + | while (hid_descriptor_it != hid_descriptors.end()) | ||
| + | { | ||
| + | uint16_t uuid16 = hid_descriptor_it-> | ||
| + | if (uuid16 == 0) | ||
| + | { | ||
| + | uuid16 = little_endian_read_16(hid_descriptor_it-> | ||
| + | } | ||
| + | if (uuid16 == 0x2902) // UUIDがClient Characteristic Configurationの場合 | ||
| + | { | ||
| + | gatt_cccd_descriptor = *hid_descriptor_it++; | ||
| + | // Notificationを有効化する値を書き込む | ||
| + | gatt_client_write_value_of_characteristic(& | ||
| + | break; | ||
| + | } | ||
| + | hid_descriptor_it++; | ||
| + | } | ||
| + | if (hid_descriptor_it == hid_descriptors.end()) // HID Reportのイテレータを進める | ||
| + | { | ||
| + | hid_descriptors.clear(); | ||
| + | while (hid_characteristic_it != hid_characteristics.end()) | ||
| + | { | ||
| + | // HID ReportのUUIDを確認 | ||
| + | if (hid_characteristic_it-> | ||
| + | uint16_t characteristic_handle = hid_characteristic_it-> | ||
| + | uint8_t properties = hid_characteristic_it-> | ||
| + | // | ||
| + | if (properties & ATT_PROPERTY_NOTIFY) { // Notificationをサポートしているか確認 | ||
| + | cur_characteristic = *hid_characteristic_it; | ||
| + | gatt_client_discover_characteristic_descriptors(& | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | hid_characteristic_it++; | ||
| + | } | ||
| + | if (hid_characteristic_it == hid_characteristics.end()) | ||
| + | { | ||
| + | gatt_client_listen_for_characteristic_value_updates(& | ||
| + | & | ||
| + | connection_handle, | ||
| + | nullptr); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | break; | ||
| + | default: | ||
| + | break; | ||
| + | |||
| + | } | ||
| + | } | ||
| + | |||
| + | // descriptor を見つけた | ||
| + | static void | ||
| + | handle_gatt_descriptors_discovered(uint8_t packet_type, | ||
| + | { | ||
| + | if (packet_type != HCI_EVENT_PACKET) return; | ||
| + | switch (hci_event_packet_get_type(packet)) | ||
| + | { | ||
| + | case GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT: | ||
| + | { | ||
| + | // Descriptorの情報を取得 | ||
| + | gatt_client_characteristic_descriptor_t descriptor; | ||
| + | gatt_event_all_characteristic_descriptors_query_result_get_characteristic_descriptor(packet, | ||
| + | hid_descriptors.push_back(descriptor); | ||
| + | } | ||
| + | break; | ||
| + | case GATT_EVENT_QUERY_COMPLETE: | ||
| + | { | ||
| + | uint16_t status = gatt_event_query_complete_get_att_status(packet); | ||
| + | if (status != 0) | ||
| + | { | ||
| + | Serial.printf(" | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | // Notificationを有効化する値を書き込む | ||
| + | hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); | ||
| + | for(hid_descriptor_it = hid_descriptors.begin(); | ||
| + | { | ||
| + | uint16_t uuid16 = hid_descriptor_it-> | ||
| + | if (uuid16 == 0) | ||
| + | { | ||
| + | uuid16 = little_endian_read_16(hid_descriptor_it-> | ||
| + | } | ||
| + | if (uuid16 == 0x2902) | ||
| + | { // UUIDがClient Characteristic Configurationの場合 | ||
| + | gatt_cccd_descriptor = *hid_descriptor_it; | ||
| + | hid_descriptor_it++; | ||
| + | gatt_client_write_value_of_characteristic(& | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | if (hid_descriptor_it == hid_descriptors.end()) | ||
| + | { | ||
| + | hid_descriptors.clear(); | ||
| + | while (hid_characteristic_it != hid_characteristics.end()) | ||
| + | { | ||
| + | if (hid_characteristic_it-> | ||
| + | uint16_t characteristic_handle = hid_characteristic_it-> | ||
| + | uint8_t properties = hid_characteristic_it-> | ||
| + | if (properties & ATT_PROPERTY_NOTIFY) { // Notificationをサポートしているか確認 | ||
| + | cur_characteristic | ||
| + | hci_con_handle_t connection_handle | ||
| + | gatt_client_discover_characteristic_descriptors(& | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | ++hid_characteristic_it; | ||
| + | } | ||
| + | if (hid_characteristic_it | ||
| + | { | ||
| + | gatt_client_listen_for_characteristic_value_updates(& | ||
| + | & | ||
| + | connection_handle, | ||
| + | nullptr); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | break; | ||
| + | default: | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| ==== Notification handler ==== | ==== Notification handler ==== | ||
| + | ほぼ蛇足であるが、Notification handlerについても少しだけ記しておく。 | ||
| + | Notificationについては、キーボードからの入力は 0x0040 のハンドルで渡されるようである。 | ||
| + | メディアキーなどの特殊キーはさらに別のハンドルとなるがここでは割愛する。 | ||
| + | |||
| + | ほとんどのキーボードの場合はデータ長は 8bytes で先頭が modifierで、1byteのパディングがあって、6bytesのキーコードがわたされる。 | ||
| + | |||
| + | 但し、バッファローのキーボードで11bytes (modifier + 10bytesのキーコード)というものがあったのでどちらでも扱えるようにデザインしてある。 | ||
| + | |||
| + | キーコードについては、キーボードごとに違いはないので、キーコードを文字にマッピングしてやって、keybuf というキューにデータを登録している。 | ||
| + | |||
| + | キーの状態に変化がないと Notificationは来ない。 | ||
| + | キーが押しっぱなしでは状態が変化したわけではないので、押しっぱなしの通知は来ない。 | ||
| + | 通知がなければ、押しっぱなしとみることで、キーリピートやゲームでのキー処理にも対応できるが、ここではそのあたりの処理はしない。 | ||
| + | |||
| + | キーを押し続けているときに新しくキーが押されたらその分が新しく押されたキーコードとして渡されるが、その時、押しっぱなしになっているキーも渡されるので、差分を見なければ、新しく押されたキーを見つけることができない。 | ||
| + | |||
| + | そのため前回渡されたキーバッファの内容を保存している。 | ||
| + | |||
| + | 押しっぱなしのキーが離されたときにも、通知が来るが、この時他に押しっぱなしのキーがあればそれも渡されるため、同様に、新しく押されたキーがあるかどうか、離されただけなのか、というチェックを前回のデータと比較して行う。 | ||
| + | |||
| + | キーボードについては概ねこのような処理を行っている。 | ||
| + | |||
| + | |||
| + | <code cpp> | ||
| + | typedef union { | ||
| + | struct __attribute__((__packed__)) | ||
| + | { | ||
| + | uint8_t modifiers; | ||
| + | uint8_t keys[10]; | ||
| + | } k1; | ||
| + | struct __attribute__((__packed__)) | ||
| + | { | ||
| + | uint8_t modifiers; | ||
| + | uint8_t reserved; | ||
| + | uint8_t keys[6]; | ||
| + | uint8_t padding[3]; | ||
| + | } k2; | ||
| + | uint8_t raw[11]; | ||
| + | } keyboard_t; | ||
| + | |||
| + | static keyboard_t keyboardReport; | ||
| + | static std:: | ||
| + | static const int MAX_KEYCODE = 96; | ||
| + | const uint8_t keymap[][MAX_KEYCODE] = { | ||
| + | { 0, | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | | ||
| + | | ||
| + | }, | ||
| + | { | ||
| + | | ||
| + | 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, | ||
| + | | ||
| + | | ||
| + | | ||
| + | | ||
| + | }, | ||
| + | { | ||
| + | | ||
| + | ' | ||
| + | '#', | ||
| + | ' | ||
| + | | ||
| + | | ||
| + | }, | ||
| + | }; | ||
| + | // 通知を受け取るハンドラ | ||
| + | static void | ||
| + | notification_handler(uint8_t packet_type, | ||
| + | { | ||
| + | if (packet_type != HCI_EVENT_PACKET) return; | ||
| + | switch (hci_event_packet_get_type(packet)) | ||
| + | { | ||
| + | case GATT_EVENT_NOTIFICATION: | ||
| + | { | ||
| + | // 通知を受信した場合の処理 | ||
| + | uint16_t attribute_handle = gatt_event_notification_get_handle(packet); | ||
| + | uint16_t value_length = gatt_event_notification_get_value_length(packet); | ||
| + | const uint8_t *value = gatt_event_notification_get_value(packet); | ||
| + | uint16_t value_handle = gatt_event_notification_get_value_handle(packet); | ||
| + | uint16_t service_id = gatt_event_notification_get_service_id(packet); | ||
| + | if (attribute_handle == 0x0040) | ||
| + | { | ||
| + | if (value_length == 0) return; // データがない場合は無視 | ||
| + | if (value_length == 8 || value_length == 11) | ||
| + | { | ||
| + | keyboard_t *newKeyReport = (keyboard_t*)value; | ||
| + | int buflen = 6; | ||
| + | uint8_t *buf = keyboardReport.k2.keys; | ||
| + | uint8_t *input = newKeyReport-> | ||
| + | uint8_t mod = newKeyReport-> | ||
| + | if (value_length == 11) | ||
| + | { | ||
| + | buflen = 10; | ||
| + | buf = keyboardReport.k1.keys; | ||
| + | input = newKeyReport-> | ||
| + | mod = newKeyReport-> | ||
| + | } | ||
| + | for (int i = 0 ; i < buflen ; i++) | ||
| + | { | ||
| + | uint8_t c = input[i]; | ||
| + | if (c == 0) continue; | ||
| + | if (mod == 3) mod = 1; | ||
| + | uint8_t ch = keymap[mod][c]; | ||
| + | if (ch == 0) continue; | ||
| + | if (memchr(buf, | ||
| + | } | ||
| + | memcpy(& | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | break; | ||
| + | default: | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
bleキーボードをつなごう_btstack編.1745280184.txt.gz · 最終更新: by araki
