bleキーボードをつなごう_btstack編
差分
このページの2つのバージョン間の差分を表示します。
両方とも前のリビジョン前のリビジョン次のリビジョン | 前のリビジョン | ||
bleキーボードをつなごう_btstack編 [2025/04/22 01:09] – [プライマリサービスの取得] araki | bleキーボードをつなごう_btstack編 [2025/04/22 06:13] (現在) – [前提] araki | ||
---|---|---|---|
行 26: | 行 26: | ||
言語は C++を使用する。 | 言語は C++を使用する。 | ||
+ | なお、この文書やプログラムの作成に当たっては[[https:// | ||
+ | |||
+ | また、Copilot、Gemini、およびLM Studio上の Gemma3 12BなどAIによる支援も利用した。 | ||
+ | |||
+ | 完全なコードは[[https:// | ||
===== 処理の流れ ===== | ===== 処理の流れ ===== | ||
行 414: | 行 419: | ||
gatt_client_discover_characteristics_for_service()がデバイスに処理されると、GATT_EVENT_CHARACTERISTIC_QUERY_RESULTのイベントが発生する。 | 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> | <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を見つけた。 | // Characteristicを見つけた。 | ||
static void | static void | ||
行 460: | 行 510: | ||
} | } | ||
</ | </ | ||
- | ==== Notificationの処理準備 ==== | + | ==== 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編.1745284199.txt.gz · 最終更新: by araki