ユーザ用ツール

サイト用ツール


bleキーボードをつなごう_btstack編

差分

このページの2つのバージョン間の差分を表示します。

この比較画面へのリンク

両方とも前のリビジョン前のリビジョン
次のリビジョン
前のリビジョン
bleキーボードをつなごう_btstack編 [2025/04/22 00:39] – [接続処理] arakibleキーボードをつなごう_btstack編 [2025/04/22 06:13] (現在) – [前提] araki
行 26: 行 26:
 言語は C++を使用する。 言語は C++を使用する。
  
 +なお、この文書やプログラムの作成に当たっては[[https://vanhunteradams.com/Pico/BLE/GATT_Client.html|Building a Bluetooth GATT Client on the Pi Pico W]]および[[https://github.com/bluekitchen/btstack/tree/501e6d2b86e6c92bfb9c390bcf55709938e25ac1|btstack-1.6.2]]のサンプルプログラムを参考にした。
 +
 +また、Copilot、Gemini、およびLM Studio上の Gemma3 12BなどAIによる支援も利用した。
 +
 +完全なコードは[[https://github.com/wildtree/HHSAdvPico.git|ハイハイスクールアドベンチャー PicoCalc版]]に含まれている。
 ===== 処理の流れ ===== ===== 処理の流れ =====
  
行 253: 行 258:
  
 ==== セキュア化 ==== ==== セキュア化 ====
 +
 +=== セキュア接続開始 ===
 +
 +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)); を呼び出せばデバイス側に承認が伝わる。
 +
 +
 +<code cpp>
 +  sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); // IOキャパビリティ設定
 +  sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION); // 認証要件を設定
 +</code>
 +
 +=== セキュア接続の完了とプライマリサービスの取得要求 ===
 +
 +デバイスにセキュア接続の承認が伝わると、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, 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;
 +  }
 +}
 +</code>
 +
  
 ==== プライマリサービスの取得 ==== ==== プライマリサービスの取得 ====
 +
 +=== サービスハンドルの取得 ===
 +
 +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, 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;
 +  }
 +}
 +</code>
 +
  
 ==== 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(); 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); 
 +    } 
 +  } 
 +
 +</code> 
 + 
 +最初に、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::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; 
 +  } 
 +
 +</code> 
 +==== 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(&notification_listener, &notification_handler, connection_handle, nullptr); 
 +</code> 
 + 
 +Characteristics一つ一つに対して処理を要求することもできるようだが、ここでは一括で行う。 
 +最後の nullptrが、全部の Characteristicsに対しての要求を意味している。 
 + 
 +これであとは notification_handler でGATT_EVENT_NOTIFICATIONのイベントを処理すれば目的達成である。 
 + 
 +<code cpp> 
 +static std::list<gatt_client_characteristic_t> hid_characteristics; 
 +static std::list<gatt_client_characteristic_descriptor_t> hid_descriptors; 
 +static std::list<gatt_client_characteristic_t>::iterator hid_characteristic_it; 
 +static std::list<gatt_client_characteristic_descriptor_t>::iterator hid_descriptor_it; 
 + 
 +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, 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_QUERY_COMPLETE: 
 +      { 
 +        uint16_t status = gatt_event_query_complete_get_att_status(packet); // Replace with the correct function 
 +        if (status != 0) { 
 +          Serial.printf("GATT Notification activation failed with status %u\n", status); 
 +        } else { 
 +          uint16_t service_id = gatt_event_query_complete_get_service_id(packet); // Retrieve service ID 
 +          uint16_t handle = gatt_event_query_complete_get_handle(packet); // Retrieve handle 
 +          hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); // Retrieve connection handle 
 +          while (hid_descriptor_it != hid_descriptors.end()) 
 +          { 
 +            uint16_t uuid16 = hid_descriptor_it->uuid16; 
 +            if (uuid16 == 0) 
 +            { 
 +              uuid16 = little_endian_read_16(hid_descriptor_it->uuid128, 0); 
 +            } 
 +            if (uuid16 == 0x2902) // UUIDがClient Characteristic Configurationの場合 
 +            { 
 +              gatt_cccd_descriptor = *hid_descriptor_it++; // Client Characteristic Configuration Descriptorを保存 
 +              // Notificationを有効化する値を書き込む 
 +              gatt_client_write_value_of_characteristic(&handle_gatt_noification_activated, connection_handle, gatt_cccd_descriptor.handle, sizeof(notification_enable), notification_enable); 
 +              break; 
 +            } 
 +            hid_descriptor_it++; // イテレータを保存 
 +          } 
 +          if (hid_descriptor_it == hid_descriptors.end()) // HID Reportのイテレータを進める 
 +          { 
 +            hid_descriptors.clear(); // HID Reportのイテレータをクリア 
 +            while (hid_characteristic_it != hid_characteristics.end()) 
 +            { 
 +              // 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; 
 +                //display.printf("Found HID Report characteristic: %04x\n", it->value_handle); 
 +                if (properties & ATT_PROPERTY_NOTIFY) { // Notificationをサポートしているか確認 
 +                  cur_characteristic = *hid_characteristic_it; // HID Reportを保存 
 +                  gatt_client_discover_characteristic_descriptors(&handle_gatt_descriptors_discovered, connection_handle, &*hid_characteristic_it++); 
 +                  break; 
 +                } 
 +              } 
 +              hid_characteristic_it++; // イテレータを保存 
 +            } 
 +            if (hid_characteristic_it == hid_characteristics.end()) 
 +            { 
 +              gatt_client_listen_for_characteristic_value_updates(&notification_listener,  
 +                &notification_handler,  
 +                connection_handle,  
 +                nullptr); 
 +            } 
 +          } 
 +        } 
 +      } 
 +      break; 
 +    default: 
 +      break; 
 + 
 +  } 
 +
 + 
 +// descriptor を見つけた 
 +static void 
 +handle_gatt_descriptors_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_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT: 
 +      { 
 +        // Descriptorの情報を取得 
 +        gatt_client_characteristic_descriptor_t descriptor; 
 +        gatt_event_all_characteristic_descriptors_query_result_get_characteristic_descriptor(packet, &descriptor); 
 +        hid_descriptors.push_back(descriptor); // 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 Descriptor query failed with status %u\n", status); 
 +        }  
 +        else  
 +        { 
 +          // Notificationを有効化する値を書き込む 
 +          hci_con_handle_t connection_handle = gatt_event_query_complete_get_handle(packet); 
 +          for(hid_descriptor_it = hid_descriptors.begin(); hid_descriptor_it != hid_descriptors.end(); ++hid_descriptor_it)  
 +          { 
 +            uint16_t uuid16 = hid_descriptor_it->uuid16; 
 +            if (uuid16 == 0) 
 +            { 
 +              uuid16 = little_endian_read_16(hid_descriptor_it->uuid128, 0); 
 +            } 
 +            if (uuid16 == 0x2902)  
 +            { // UUIDがClient Characteristic Configurationの場合 
 +              gatt_cccd_descriptor = *hid_descriptor_it; // Client Characteristic Configuration Descriptorを保存 
 +              hid_descriptor_it++; // イテレータを保存 
 +              gatt_client_write_value_of_characteristic(&handle_gatt_noification_activated, connection_handle, gatt_cccd_descriptor.handle, sizeof(notification_enable), notification_enable); 
 +              break; 
 +            } 
 +          } 
 +          if (hid_descriptor_it == hid_descriptors.end())  
 +          { 
 +            hid_descriptors.clear(); // HID Reportのイテレータをクリア 
 +            while (hid_characteristic_it != hid_characteristics.end()) 
 +            { 
 +              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をサポートしているか確認 
 +                  cur_characteristic *hid_characteristic_it; // HID Reportを保存 
 +                  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; 
 +                } 
 +              } 
 +              ++hid_characteristic_it; // イテレータを保存 
 +            } 
 +            if (hid_characteristic_it == hid_characteristics.end()) 
 +            { 
 +              gatt_client_listen_for_characteristic_value_updates(&notification_listener,  
 +                &notification_handler,  
 +                connection_handle,  
 +                nullptr); 
 +            } 
 +          } 
 +        } 
 +      } 
 +      break; 
 +    default: 
 +      break; 
 +  } 
 +
 +</code>
  
 ==== 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::queue<uint8_t> keybuf;
 +static const int MAX_KEYCODE = 96;
 +const uint8_t keymap[][MAX_KEYCODE] = {
 +  {    0,   0,   0,   0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
 +     'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2',
 +     '3', '4', '5', '6', '7', '8', '9', '0',  13,  27,   8,   9, ' ', '-', '=', '[',
 +     ']','\\',   0, ';','\'', '`', ',', '.', '/',   0,   0,   0,   0,   0,   0,   0,
 +       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0, 127,   0,   0,   0,
 +       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
 +  },
 +  {
 +       0,   0,   0,   0,   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,   0,   0,
 +       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
 +       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
 +       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
 +       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
 +  },
 +  {
 +       0,   0,   0,   0, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
 +     'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '!', '@',
 +     '#', '$', '%', '^', '&', '*', '(', ')',  13,  27,   8,   9, ' ', '_', '+', '{',
 +     '}', '|',   0, ':', '"', '~', '<', '>', '?',   0,   0,   0,   0,   0,   0,   0,
 +       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0, 127,   0,   0,   0,
 +       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
 +  },
 +};
 +// 通知を受け取るハンドラ
 +static void 
 +notification_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 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->k2.keys;
 +            uint8_t mod = newKeyReport->k2.modifiers;
 +            if (value_length == 11)
 +            {
 +              buflen = 10;
 +              buf = keyboardReport.k1.keys;
 +              input = newKeyReport->k1.keys;
 +              mod = newKeyReport->k1.modifiers;     
 +            }
 +            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, c, buflen) == NULL) keybuf.push(ch);
 +            }
 +            memcpy(&keyboardReport, value, value_length);
 +          }
 +        }
 +      }
 +      break;
 +    default:
 +      break;
 +  }
 +}
 +</code>
  
  
bleキーボードをつなごう_btstack編.1745282398.txt.gz · 最終更新: by araki