ユーザ用ツール

差分

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

この比較画面へのリンク

両方とも前のリビジョン前のリビジョン
次のリビジョン
前のリビジョン
bleキーボードをつなごう_btstack編 [2025/04/22 01:34] – [Notificationの処理準備] arakibleキーボードをつなごう_btstack編 [2026/05/26 23:45] (現在) – [処理の流れ] 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版]]に含まれている。
 ===== 処理の流れ ===== ===== 処理の流れ =====
  
行 40: 行 45:
 この時、デバイスによっては接続を暗号化してセキュアにする必要がある場合がある。 この時、デバイスによっては接続を暗号化してセキュアにする必要がある場合がある。
 キーボードの場合は、その性質上打鍵情報を傍受される危険性があるためか、セキュアであることを要求しているものが少なくない。 キーボードの場合は、その性質上打鍵情報を傍受される危険性があるためか、セキュアであることを要求しているものが少なくない。
-手元にあるキーボードでは、バッファロー製のものと、Capdputerを購入したときに入っているサンプルのBLEキーボード化アプリがセキュア接続を要求しない((前者はセキュアでもOKだが後者はセキュア接続がそもそもできない))が、他はすべてセキュア接続を要求する。+手元にあるキーボードでは、バッファロー製のものと、Cardputerを購入したときに入っているサンプルのBLEキーボード化アプリがセキュア接続を要求しない((前者はセキュアでもOKだが後者はセキュア接続がそもそもできない))が、他はすべてセキュア接続を要求する。
 この要件は、キー入力を通知する Characteristic の属性に記されており、セキュア接続を必須とする場合に、セキュアでない接続上でキー入力のデータを要求しても要求が失敗する。 この要件は、キー入力を通知する Characteristic の属性に記されており、セキュア接続を必須とする場合に、セキュアでない接続上でキー入力のデータを要求しても要求が失敗する。
  
行 507: 行 512:
 ==== Descriptor一覧の取得とNotificationの要求 ==== ==== Descriptor一覧の取得とNotificationの要求 ====
  
-==== 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>
  
  

This website uses cookies. By using the website, you agree with storing cookies on your computer. Also, you acknowledge that you have read and understand our Privacy Policy. If you do not agree, please leave the website.

More information