Realforce for Mac を BLE で無線接続して 2台の Mac で使う

更新した記事を[こちら](https://qiita.com/mgf/items/a9e462f3b2ab35903de6)に書きました。
以下は過去の記事です。

最近在宅で仕事をすることが多いので、入力環境の改善を図ろうとキーボードをいろいろ検討していました。
結局 Realforce TKL SA for Mac に落ち着いたのですが、このキーボードは USB接続のため複数の Mac で簡単に切り替えて使えません。
USB2BT っていうのが売ってるのですが、結構なお値段がするので、自分で作ってみることにしました。
# 準備するもの
## ハードウェア

- [M5Stack Basic](https://www.switch-science.com/catalog/3647/)
- [M5Stack用MAX3421E搭載 USBモジュール](https://www.switch-science.com/catalog/6061/)

リンク先はスイッチサイエンスです。
## ソフトウェア

- Arduino 1.8.12
- M5Stack Library 0.3.0
- USB Host Shield Library 2.0 1.3.2
- NimBLE-Arduino 1.0.2

# 使い方

- あとの方に書いてあるように USB Host Shield 2.0 を少し変更します。Media Key が使えなくてもよいなら変更しなくてもよいです。

- inoファイルの <== change のところを接続したい Mac のアドレスに書き換えます。
アドレスは
`void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) `
の中の
`const char *currentname = NimBLEAddress(desc->peer_ota_addr).toString().c_str();`
でとってきたものを比較してますので、ここで取れたものに書き換えてください。
- F14とF15で2台の Mac を切り替えてます。切り替えはうまくいけばほぼ一瞬で切り替えできます。
- Logicool の Flow に擬似的に対応していて、Flow を Ctrl を押した場合のみ移動する設定にしておけば、Option + Ctrl を押しながら移動させるとキーボードも切り替えます。Option + Ctrl を500ms以上長押ししていると今つながっていない方の Mac に切り替えるだけですが、そこそこ使えます。
- たまにうまく切り替えできなくなることがあるのですが、その時は M5Stack をリセットしています。 
- 本当はBLEアドレスを変更して切り替えたかったのですが、うまくできなかったので、Advertisingして目的のMacに接続されたらそれ以外の接続を切るようにしています。どなたか複数のアドレスで切り替える方法ご存知の方がいたら教えてほしいです。
- 現在の私の使用方法では2台の切り替えで十分なのですが、必要であれば3台以上でも大丈夫だと思います。

# USB Host Shield 2.0 の変更
USB Host Shield Library 2.0 に付属のサンプルで Realforce for Mac からの入力を取り扱おうとしたところ、Media 関連のキーがうまくとれません。最初は Media <-> Fn で Fn にしておいて扱おうかと思ったのですが、調べてみたところ、USB Host Shield Library 2.0 の HID では複数のインターフェースがあるとうまく動作していないようでしたので、以下を変更しました。コードは綺麗じゃないですけど。

hiduniversal.h 54行あたり prevBuf を複数インターフェース分確保 (とりあえず10)

```C++:hiduniversal.h
//        uint8_t prevBuf[constBuffLen]; // previous event buffer
        uint8_t prevBuf[10][constBuffLen]; // previous event buffer
```
hiduniversal.cpp の 71行あたり prevBuf をインターフェース分クリア

```C++:hiduniversal.cpp
        for (int i = 0; i < 10; i++) {
                ZeroMemory(constBuffLen, prevBuf[i]);
        }
```
389 行あたり inTransition  で rcode が4でも次のインターフェースに進む

```C++:
                        uint8_t rcode = pUsb->inTransfer(bAddress, epInfo[index].epAddr, &read, buf);

                        if (rcode) {
                                if(rcode != hrNAK)
                                        USBTRACE3("(hiduniversal.h) Poll:", rcode, 0x81);
                                if (rcode == 4) {
                                        continue;
                                }
                                return rcode;
                        }
```
403行あたり インターフェースごとのprevBufを使うように変更し、前と同じ場合は次のインターフェースに進むように変更

```C++:
                        bool identical = BuffersIdentical(read, buf, prevBuf[i]);

                        SaveBuffer(read, buf, prevBuf[i]);

                        if (identical) {
                                continue;
                                // return 0;
                        }
```

#ソースコード
Arduinoのソースコードです。

```C++:M5Stack_Realforce_BLE.ino
/*
MIT License

Copyright (c) 2020 Satoru Sato

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <M5Stack.h>
#include <hiduniversal.h>
#include <NimBLEDevice.h>
#include "HIDKeyboardTypes.h"
#include "HIDTypes.h"
#include "Free_Fonts.h"
#include <EEPROM.h>

#define REALFORCE_VID 0x0853
#define REALFORCE_PID 0x0105

#define HID_KEYBOARD 0x03C1

// Report ID
#define KEYBOARD_ID 0x01
#define MEDIA_KEYS_ID 0x03

static const uint8_t reportMap[] = {
  USAGE_PAGE(1),      0x01,          // USAGE_PAGE (Generic Desktop)
  USAGE(1),           0x06,          // USAGE (Keyboard)
  COLLECTION(1),      0x01,          // COLLECTION (Application) Start Keyboard Collection
  // ------------------------------------------------- Keyboard
  USAGE_PAGE(1),      0x07,          //   USAGE_PAGE (Kbrd/Keypad)
  REPORT_ID(1),       KEYBOARD_ID,   //   REPORT_ID (1) Report ID = 1 (Keyboard)
  USAGE_MINIMUM(1),   0xE0,          //   USAGE_MINIMUM (0xE0)
  USAGE_MAXIMUM(1),   0xE7,          //   USAGE_MAXIMUM (0xE7)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  LOGICAL_MAXIMUM(1), 0x01,          //   LOGICAL_MAXIMUM (1)
  // ------------------------------------------------- 
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1) ; 1 byte (Modifier?)
  REPORT_COUNT(1),    0x08,          //   REPORT_COUNT (8) ; 8 bits
  HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  // ------------------------------------------------- 
  REPORT_COUNT(1),    0x01,          //   REPORT_COUNT (1) ; 1 byte (Reserved)
  REPORT_SIZE(1),     0x08,          //   REPORT_SIZE (8) ; 8 bits
  HIDINPUT(1),        0x01,          //   INPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  // ------------------------------------------------- 
  REPORT_COUNT(1),    0x05,          //   REPORT_COUNT (5) ; 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1)
  USAGE_PAGE(1),      0x08,          //   USAGE_PAGE (LEDs)
  REPORT_ID(1),       KEYBOARD_ID,   //   REPORT_ID (1) Report ID = 1 (Keyboard)
  USAGE_MINIMUM(1),   0x01,          //   USAGE_MINIMUM (0x01) ; Num Lock
  USAGE_MAXIMUM(1),   0x05,          //   USAGE_MAXIMUM (0x05) ; Kana
  HIDOUTPUT(1),       0x02,          //   OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  // ------------------------------------------------- 
  REPORT_COUNT(1),    0x01,          //   REPORT_COUNT (1) ; 3 bits (Padding)
  REPORT_SIZE(1),     0x03,          //   REPORT_SIZE (3)
  HIDOUTPUT(1),       0x03,          //   OUTPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  // ------------------------------------------------- 
  REPORT_COUNT(1),    0x06,          //   REPORT_COUNT (6) ; 6 bytes (Keys)
  REPORT_SIZE(1),     0x08,          //   REPORT_SIZE(8)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM(0)
  LOGICAL_MAXIMUM(1), 0x65,          //   LOGICAL_MAXIMUM(0x65) ; 101 keys
  USAGE_PAGE(1),      0x07,          //   USAGE_PAGE (Kbrd/Keypad)
  USAGE_MINIMUM(1),   0x00,          //   USAGE_MINIMUM (0)
  USAGE_MAXIMUM(1),   0x65,          //   USAGE_MAXIMUM (0x65)
  HIDINPUT(1),        0x00,          //   INPUT (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  // ------------------------------------------------- 
  END_COLLECTION(0),                    // END_COLLECTION
  // ------------------------------------------------- Media Keys
  USAGE_PAGE(1),      0x0C,          // USAGE_PAGE (Consumer)
  USAGE(1),           0x01,          // USAGE (Consumer Control)
  COLLECTION(1),      0x01,          // COLLECTION (Application)
  REPORT_ID(1),       MEDIA_KEYS_ID, //   REPORT_ID (3)
  USAGE_PAGE(1),      0x0C,          //   USAGE_PAGE (Consumer)
  LOGICAL_MINIMUM(1), 0x00,          //   LOGICAL_MINIMUM (0)
  LOGICAL_MAXIMUM(1), 0x01,          //   LOGICAL_MAXIMUM (1)
  REPORT_SIZE(1),     0x01,          //   REPORT_SIZE (1) ; 2 bytes
  REPORT_COUNT(1),    0x10,          //   REPORT_COUNT (16)
  USAGE(1),           0xB5,          //   USAGE (Scan Next Track)     ; bit 0: 1
  USAGE(1),           0xB6,          //   USAGE (Scan Previous Track) ; bit 1: 2
  USAGE(1),           0xB7,          //   USAGE (Stop)                ; bit 2: 4
  USAGE(1),           0xCD,          //   USAGE (Play/Pause)          ; bit 3: 8
  USAGE(1),           0xE2,          //   USAGE (Mute)                ; bit 4: 16
  USAGE(1),           0xE9,          //   USAGE (Volume Increment)    ; bit 5: 32
  USAGE(1),           0xEA,          //   USAGE (Volume Decrement)    ; bit 6: 64
  USAGE(1),           0xB8,          //   Usage (Eject)               ; bit 7: 128
  USAGE(1),           0x70,          //   Usage (Brightness Down)     ; bit 0: 1
  USAGE(1),           0x6F,          //   Usage (Brightness Up)       ; bit 1: 2
  HIDINPUT(1),        0x02,          //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  // ------------------------------------------------- 
  END_COLLECTION(0)                     // END_COLLECTION
};

static char connection_addr[2][18] = {
  "xx:xx:xx:xx:xx:xx", // MacBook Pro <== change
  "xx:xx:xx:xx:xx:xx", // MacBook Air <== change
};
static int target_connection = 0; // MacBook Pro
static uint16_t target_handle;
static int connected_count = 0;
static boolean connection_established = false;

static uint8_t current_leds = 0;
static uint8_t leds = 0;

static NimBLEServer* pServer;
static NimBLECharacteristic* pReport1;
static NimBLECharacteristic* pReport2;
static NimBLECharacteristic* pReport3;

class REALFORCE : public HIDUniversal {
public:
  REALFORCE(USB *p) : HIDUniversal(p) {};
  bool connected() {
    return HIDUniversal::isReady() &&
        HIDUniversal::VID == REALFORCE_VID && HIDUniversal::PID == REALFORCE_PID;
  };

private:
  void ParseHIDData(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf);
  uint8_t OnInitSuccessful() { // Called by the HIDUniversal library on success
    if (HIDUniversal::VID != REALFORCE_VID || HIDUniversal::PID != REALFORCE_PID) {
      return 1;
    }
    return 0;
  };
};

USB Usb;
REALFORCE realforce(&Usb);

class ServerCallbacks: public NimBLEServerCallbacks {
  void onConnect(NimBLEServer* pServer) {
    // なにかが接続した時点で一旦 Advertising は Stop する
    NimBLEDevice::stopAdvertising();
  };
  
  void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) {
    const char *currentname = NimBLEAddress(desc->peer_ota_addr).toString().c_str();

    // 接続先切り替え
    connected_count++; // 接続先カウントをインクリメント
    if (connection_established) {
      // すでに接続しているのに connect されたら connect しているものも disconnect してやりなおす。
      // そうしないとなぜかうまくいかない
      pServer->disconnect(desc->conn_handle);
      M5.Lcd.fillRect(0, 70, 320, 60, BLACK);
      pServer->disconnect(target_handle);
      connection_established = false;
    }
    else if (strcmp(connection_addr[target_connection], currentname) == 0) {
      target_handle = desc->conn_handle;
      M5.Lcd.setTextColor(LIGHTGREY);
      M5.Lcd.setTextDatum(CC_DATUM);
      M5.Lcd.setFreeFont(FF30);
      M5.Lcd.fillRect(0, 70, 320, 60, BLACK);
      if (target_connection == 0) {
        M5.Lcd.drawString("MacBook Pro", 160, 100, GFXFF);
      }
      else if (target_connection == 1) {
        M5.Lcd.drawString("MacBook Air", 160, 100, GFXFF);
      } 
      connection_established = true;
      pServer->updateConnParams(desc->conn_handle, 0x10, 0x20, 0, 600);
    }
    else {
      pServer->disconnect(desc->conn_handle);
      NimBLEDevice::startAdvertising();
    }
  };
  
  void onDisconnect(NimBLEServer* pServer) {
    connected_count--;
    if (connected_count == 0) {
      M5.Lcd.fillRect(0, 70, 320, 60, BLACK);
      connection_established = false;
      NimBLEDevice::startAdvertising();
    }
  };

  void onAuthenticationComplete(ble_gap_conn_desc* desc) {
    if (!desc->sec_state.encrypted) {
      // これがきたらおかしくなるので、EEPROMにtarget_connectionを記録してResetする
      EEPROM.put(0, target_connection);
      EEPROM.commit();
      M5.Power.reset(); // Reset
      return;
    }
  };
};

class CharacteristicCallbacks: public NimBLECharacteristicCallbacks {
  void onWrite(NimBLECharacteristic* pCharacteristic) {
    std::string str;
    str = pReport3->getValue();
    leds = *(uint8_t *)str.c_str();
  };
};

static CharacteristicCallbacks chrCallbacks;

void sendKey(uint8_t *key) {
  if (pServer->getConnectedCount() && connection_established) {
    pReport1->setValue(key, 8);
    pReport1->notify();
  }
}

void sendMediaKey(uint8_t *mediakey) {
  if (pServer->getConnectedCount() && connection_established) {
    pReport2->setValue(mediakey, 2);
    pReport2->notify();
  }
}

// USB
void REALFORCE::ParseHIDData(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) {
  uint8_t mediakey[2];
  static boolean modifyflag = false;
  static boolean flowflag = false; // Logicool Flow Support
  static unsigned long flowstart = 0;

  if (len == 8) {
    boolean releaseallflag = true;
    for (int i = 1; i < 8; i++) {
      if (buf[i] != 0) {
        releaseallflag = false;
      }
    }
    // Logicool Flow suport
    if (buf[0] == 0x05 && buf[2] == 0x00 && buf[3] == 0x00) { // 左 Option + 左 Ctrl
      flowflag = true;
      flowstart = millis();
    }
    else if (flowflag) {
      if (buf[0] == 0x05 && (buf[2] != 0x00 || buf[3] != 0x00)) { // 違うキーが押されたのでクリア
        flowflag = false;
      }
      else if (buf[0] == 0x00 && buf[2] == 0x00 && buf[3] == 0x00) { // リリース
        flowflag = false;
        if (millis() - flowstart > 500) {
          sendKey(buf);
           // 切り替え
          M5.Lcd.fillRect(0, 70, 320, 60, BLACK);
          if (connection_established) {
            pServer->disconnect(target_handle);
            connection_established = false;
          }
          if (target_connection == 0) {
            target_connection = 1;
          }
          else {
            target_connection = 0;
          }
          NimBLEDevice::startAdvertising();
          return;
        }
      }
      else {
        sendKey(buf);
      }
    }
    
    // 接続先切り替えキー
    if (buf[0] == 0 && (buf[2] == 0x69 || buf[3] == 0x69)) {
      // F14 単独
      if (target_connection != 0) {
        M5.Lcd.fillRect(0, 70, 320, 60, BLACK);
        if (connection_established) {
          pServer->disconnect(target_handle);
          connection_established = false;
        }
        target_connection = 0;
        NimBLEDevice::startAdvertising();
      }              
    }
    else if (buf[0] == 0 && (buf[2] == 0x6a || buf[3] == 0x6a)) {
      // F15 単独
      if (target_connection != 1) {
        M5.Lcd.fillRect(0, 70, 320, 60, BLACK);
        if (connection_established) {
          pServer->disconnect(target_handle);
          connection_established = false;
        }
        target_connection = 1;
        NimBLEDevice::startAdvertising();
      }              
    }
    else {
      if (releaseallflag && modifyflag) {
        mediakey[0] = 0;
        mediakey[1] = 0;
        sendMediaKey(mediakey);
        modifyflag = false;  
      }
      sendKey(buf);
    }
  }
  else if (len == 2) {
    if (buf[1] == 2) {
      // Brightness Down
      mediakey[0] = 0;
      mediakey[1] = 1; // Brightness Down
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    if (buf[1] == 1) {
      // Brightness Down
      mediakey[0] = 0;
      mediakey[1] = 2; // Brightness Up
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    if (buf[1] == 4) {
      // Eject
      mediakey[0] = 128; // Eject
      mediakey[1] = 0;      
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    else if (buf[1] == 0) {
      // release
      mediakey[0] = 0;
      mediakey[1] = 0;
      sendMediaKey(mediakey);
      modifyflag = false;
    }
  }
  else if (len == 3) {
    if (buf[1] == 0x04) {
      mediakey[0] = 2; // Previous Track
      mediakey[1] = 0;
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    else if (buf[1] == 0x01) {
      mediakey[0] = 8; // Play / Pause
      mediakey[1] = 0;
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    else if (buf[1] == 0x08) {
      mediakey[0] = 1; // Next Track
      mediakey[1] = 0;
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    else if (buf[1] == 0x10) {
      mediakey[0] = 16; // Mute
      mediakey[1] = 0;
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    else if (buf[1] == 0x20) {
      mediakey[0] = 64; // Volume Down
      mediakey[1] = 0;
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    else if (buf[1] == 0x40) {
      mediakey[0] = 32; // Volume Up
      mediakey[1] = 0;
      sendMediaKey(mediakey);
      modifyflag = true;
    }
    else if (buf[1] == 0) {
      // release
      mediakey[0] = 0;
      mediakey[1] = 0;
      sendMediaKey(mediakey);
      modifyflag = false;
    }
  }
}

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setBrightness(20);
  // EEPROM support
  EEPROM.begin(4); // 4 byte EEPROM data
  EEPROM.get(0, target_connection);
  if (target_connection < 0 || 1 < target_connection) {
    target_connection = 0;
  }
  if (Usb.Init() == -1) {
    Serial.println("OSC did not start.");
  }
  delay(200);

  Serial.println("Starting RealForce to Bluetooth");
  NimBLEDevice::init("RealForce BT");
  NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND);
  pServer = NimBLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());
  // DeviceInfo Service
  NimBLEService* pDeviceInfoService = pServer->createService("180A");
  // DeviceInfo Service - pnp
  NimBLECharacteristic* pPnpCharacteristic = pDeviceInfoService->createCharacteristic("2A50",
      NIMBLE_PROPERTY::READ);
  uint8_t sig = 0x02;
  uint16_t vid = 0x5308; // 東プレ
  uint16_t pid = 0x5101; // RealForce for Mac 
  uint16_t version = 0x0001; 
  uint8_t pnp[] = {sig, (uint8_t) (vid >> 8), (uint8_t) vid, (uint8_t) (pid >> 8), (uint8_t) pid, (uint8_t) (version >> 8), (uint8_t) version };
  pPnpCharacteristic->setValue(pnp, sizeof(pnp));

  // DeviceInfo Service - Manufacturer
  NimBLECharacteristic* pManufacturerCharacteristic = pDeviceInfoService->createCharacteristic("2A29", 
      NIMBLE_PROPERTY::READ);
  pManufacturerCharacteristic->setValue("M5Stack");

  // HID Service
  NimBLEService* pHidService = pServer->createService(NimBLEUUID("1812"), 40);
  // HID Service - HID Information
  NimBLECharacteristic* pHidInfoCharacteristic = pHidService->createCharacteristic("2A4A", 
      NIMBLE_PROPERTY::READ);
  uint8_t country = 0x00;
  uint8_t flags = 0x01;
  uint8_t info[] = {0x11, 0x1, country, flags};                                              
  pHidInfoCharacteristic->setValue(info, sizeof(info));

  // HID Service - Report Map
  NimBLECharacteristic* pReportMapCharacteristic = pHidService->createCharacteristic("2A4B", 
      NIMBLE_PROPERTY::READ);
  pReportMapCharacteristic->setValue((uint8_t *)reportMap, sizeof(reportMap));

  // HID Service - HID Control Point
  pHidService->createCharacteristic("2A4C", NIMBLE_PROPERTY::WRITE_NR);

  // HID Service - Protocol Mode
  NimBLECharacteristic* pProtocolModeCharacteristic = pHidService->createCharacteristic("2A4E", 
      NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::READ);
  const uint8_t pMode[] = {0x01}; // 0: Boot Protocol 1: Rport Protocol
  pProtocolModeCharacteristic->setValue((uint8_t *)pMode, 1);                    

  // HID Service - Report 1
  NimBLECharacteristic* pInputCharacteristic1 = pHidService->createCharacteristic("2A4D", 
      NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC);                                      
  pReport1 = pInputCharacteristic1;

  // Report Descriptor 1
  NimBLEDescriptor* pDesc1 = pInputCharacteristic1->createDescriptor("2908",
      NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20);
  uint8_t desc1_val[] = {1, 0x01}; // Report ID 1 を Input に設定
  pDesc1->setValue((uint8_t*)desc1_val, 2);

  // HID Service - Report 2
  NimBLECharacteristic* pInputCharacteristic2 = pHidService->createCharacteristic("2A4D", 
      NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC);                                      
  pReport2 = pInputCharacteristic2;
  
  // Report Descriptor 2
  NimBLEDescriptor* pDesc2 = pInputCharacteristic2->createDescriptor("2908",
      NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20);
  uint8_t desc2_val[] = {3, 0x01}; // Report ID 3 を Input に設定
  pDesc2->setValue((uint8_t*) desc2_val, 2);

  // HID Service - Report 3
  NimBLECharacteristic* pOutputCharacteristic = pHidService->createCharacteristic("2A4D",
      NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC);
  pOutputCharacteristic->setCallbacks(&chrCallbacks);
  pReport3 = pOutputCharacteristic;

  // Report Descriptor 3
  NimBLEDescriptor* pDesc3 = pOutputCharacteristic->createDescriptor("2908",
      NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20);
  uint8_t desc3_val[] = {1, 0x02}; // Report ID 1 を Output に設定
  pDesc3->setValue((uint8_t*) desc3_val, 2);

  pDeviceInfoService->start();
  pHidService->start();
  pServer->advertiseOnDisconnect(false);
  NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
  pAdvertising->setAppearance(HID_KEYBOARD);
  pAdvertising->addServiceUUID(pHidService->getUUID());
  pAdvertising->setScanResponse(false);
  pAdvertising->start();
}

void loop() {
  Usb.Task();
  if (current_leds != leds) {
    realforce.SetReport(0, 0, 0x02, 0, 1, &leds);
    current_leds = leds;
  }
}
```