NimBLEでESP32(M5StickC)にBLEデバイスを接続する(複数デバイス対応)

ESP32のBLEはNimBLEに切り替わっていくらしい(ESP-IDF4にも同梱された)という話を聞いたので試してみました

ここではM5StickCを使っていますがESP32が載っていれば何でも使えると思います

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ヤッター<a href="https://twitter.com/hashtag/NimBLE?src=hash&amp;ref_src=twsrc%5Etfw">#NimBLE</a> でBLE HIDデバイス(ダイソーシャッター)3台接続できたよー＼(^o^)／<a href="https://twitter.com/hashtag/M5StickC?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickC</a> <a href="https://t.co/3912KDYsLm">pic.twitter.com/3912KDYsLm</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1302300693303181312?ref_src=twsrc%5Etfw">September 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

bCore4にBLE接続して操作できました
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/bCore4?src=hash&amp;ref_src=twsrc%5Etfw">#bCore4</a> と <a href="https://twitter.com/hashtag/M5StickC?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickC</a> のフェーダーUnitでモタチカ出来ました＼(^o^)／<a href="https://twitter.com/hashtag/%E3%83%A2%E3%82%BF%E3%83%81%E3%82%AB?src=hash&amp;ref_src=twsrc%5Etfw">#モタチカ</a> <a href="https://twitter.com/hashtag/%E3%81%A8%E3%81%AF?src=hash&amp;ref_src=twsrc%5Etfw">#とは</a> <a href="https://t.co/fU2TobRvl1">pic.twitter.com/fU2TobRvl1</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1533402363335438336?ref_src=twsrc%5Etfw">June 5, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##NimBLEのインストール##

Arduino IDEのライブラリマネージャでNimBLEを検索してインストールします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/83008baa-fbc7-a3b1-f66d-591a6fdfe641.png)


##サンプルを修正する##

ファイル>スケッチ例>NimBLE-Arduino>NimBLE_Clientを開きます

コードはこちらになります
NimBLE-Arduino/NimBLE_Client.ino at master · h2zero/NimBLE-Arduino · GitHub
https://github.com/h2zero/NimBLE-Arduino/blob/master/examples/NimBLE_Client/NimBLE_Client.ino

当然このままでは動かないので修正します

HIDで接続してReport Charcteristicを使うのでUUIDを定義します

```cpp

// UUID HID
static NimBLEUUID serviceUUID("1812");
// UUID Report Charcteristic
static NimBLEUUID charUUID("2a4d");

```

NimBLEAdvertisedDeviceCallbacksの中をHID UUIDを使うように修正します

```cpp

class AdvertisedDeviceCallbacks: public NimBLEAdvertisedDeviceCallbacks {

  void onResult(NimBLEAdvertisedDevice *advertisedDevice) {
    Serial.print("Advertised Device found: ");
// toString()に不具合がある？
//        Serial.println(advertisedDevice->toString().c_str());
    Serial.printf("name:%s,address:%s", advertisedDevice->getName().c_str(), advertisedDevice->getAddress().toString().c_str());
    Serial.printf("UUID:%s\n", advertisedDevice->haveServiceUUID() ? advertisedDevice->getServiceUUID().toString().c_str() : "none");

    // HID UUIDかチェックして正しければスキャンをストップ
    if (advertisedDevice->isAdvertisingService(serviceUUID)) {
      Serial.println("Found Our Service");
      /** stop scan before connecting */
      NimBLEDevice::getScan()->stop();
      /** Save the device reference in a global for the client to use*/
      advDevice = advertisedDevice;
      /** Ready to connect now */
      doConnect = true;
    }
  }
  ;
};

```

connectToServer()中でUUIDの"DEAD","BEEF","BAAD","F00D"など処理してるところをHIDのUUIDを使って書き換えます

また、サンプルではライブラリが一つのUUIDに複数のCharacteristicが存在することを想定していません

以下の画像ではHID UUID(1812)の下に複数のReport Characteristic(2a4d)が存在しているのですが、
(1つしかないものもあると思われる)
偶然正しいCharacteristicを引かないと絶対に動きません
ちなみにこの画像ではdescriptorが2個ある一番上が正解です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/32cd38ea-9968-5581-33ec-ad70714e34d0.png)

なのでCharacteristicを取得する部分を複数取得するように修正します
ライブラリを見るとstd::vector<NimBLERemoteCharacteristic*>* NimBLERemoteService::getCharacteristics(bool refresh)がありましたのでこれを使います

196行目以降をこのように変更します

```cpp

  NimBLERemoteService *pSvc = nullptr;
// 複数扱うためにvectorを使う
//  NimBLERemoteCharacteristic *pChr = nullptr;
  std::vector<NimBLERemoteCharacteristic*> *pChrs = nullptr;

  NimBLERemoteDescriptor *pDsc = nullptr;

  // HIDサービスを取得する
  pSvc = pClient->getService(serviceUUID);
  if (pSvc) { /** make sure it's not null */
    // 複数のCharacterisiticsを取得(リフレッシュtrue)
    pChrs = pSvc->getCharacteristics(true);
  }

  if (pChrs) { /** make sure it's not null */
    // 複数のReport Characterisiticsの中からNotify属性を持っているものをCallbackに登録する
    for (int i = 0; i < pChrs->size(); i++) {

      if (pChrs->at(i)->canNotify()) {
        if (!pChrs->at(i)->registerForNotify(notifyCB)) {
          /** Disconnect if subscribe failed */
          pClient->disconnect();
          return false;
        }
      }
    }
  }

```

ここまで修正するとHIDデバイスと接続できるようになります
ダイソーで売っているBLEリモートシャッターを例にすると
ボタンを押すたびにnotifyCB()が実行されデータが取得できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/a4e6662b-7cd3-47d7-f4b0-3fe26b17fe64.png)


```cpp

void notifyCB(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
  std::string str = (isNotify == true) ? "Notification" : "Indication";
  str += " from ";
  str += pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress().toString();
  str += ": Service = " + pRemoteCharacteristic->getRemoteService()->getUUID().toString();
  str += ", Characteristic = " + pRemoteCharacteristic->getUUID().toString();
  Serial.print(str.c_str());
  Serial.print("\ndata: ");
  for (int i = 0; i < length; i++) {
    // uint8_tを頭0のstringで表示する
    Serial.printf("%02X ", pData[i]);
  }
  Serial.print("\n");
}

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/03714108-5a34-47f5-0d0b-0e212c4b6760.png)

ちなみにBLEのHIDデバイスだったら何でも繋がります
実際に運用する場合はnameかaddressをチェックするとよいと思います

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">NimBLEでBLE HIDコントローラとM5StickC繋がったよ＼(^o^)／<br><br>サンプル修正だけでライブラリ修正は必要なかったです <a href="https://t.co/kgTu1lhgSA">pic.twitter.com/kgTu1lhgSA</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1287392676355088385?ref_src=twsrc%5Etfw">July 26, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##M5StickCを操作する##

HIDデバイスから入力データを取得できるようになったので
M5StickCを操作してみます

GPIO10に赤色LEDが接続されているのでそちらを点滅させてみます

ダイソーシャッターは

iOSボタン(大)を押すと

```
Notification from ff:ff:c3:19:fa:0b: Service = 0x1812, Characteristic = 0x2a4d
data: 01 00 
Notification from ff:ff:c3:19:fa:0b: Service = 0x1812, Characteristic = 0x2a4d
data: 00 00 
```

androidボタン(小)を押すと

```
Notification from ff:ff:c3:19:fa:0b: Service = 0x1812, Characteristic = 0x2a4d
data: 00 28 
Notification from ff:ff:c3:19:fa:0b: Service = 0x1812, Characteristic = 0x2a4d
data: 01 00 
Notification from ff:ff:c3:19:fa:0b: Service = 0x1812, Characteristic = 0x2a4d
data: 00 00 
Notification from ff:ff:c3:19:fa:0b: Service = 0x1812, Characteristic = 0x2a4d
data: 00 00 
```

という風にデータを送ってくるので00 28,01 00が来たらLEDを点灯して
01 00が来たら消灯するようにしてみます

```cpp

// 点灯したあと01 00を読み飛ばすためのフラグ
bool on = false;

/** Notification / Indication receiving handler callback */
void notifyCB(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
 Serial.print("data: ");
  for (int i = 0; i < length; i++) {
    // uint8_tを頭0のstringで表示する
    Serial.printf("%02X ", pData[i]);
  }
  Serial.print("\n");
  if (pData[1] & 0x28) {
    // 00 28ならばLEDを点灯してフラグを立てる
    digitalWrite(GPIO_NUM_10, LOW);
    on = true;
  }
  if (pData[0] & 0x01) {
    // フラグが立っている(00 28の直後)ならば読み飛ばす
    if (on) {
    on = false;
    return;
  }
    // LEDを消灯する
    digitalWrite(GPIO_NUM_10, HIGH);
  }

}
```

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/NimBLE?src=hash&amp;ref_src=twsrc%5Etfw">#NimBLE</a> でダイソーシャッター動きました<br>サンプルちょっと直したくらいですけどQiitaにまとめた方がいいですかね<br>情報も少ないし<a href="https://twitter.com/hashtag/M5StickC?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickC</a> <a href="https://t.co/d8zxPkEvmH">pic.twitter.com/d8zxPkEvmH</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1287834556540715009?ref_src=twsrc%5Etfw">July 27, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ちなみにスケッチの大きさは42%とArduino-BLEと比べてだいぶ小さくなっていると思います
多分Arduino-BLEだと70%~80%くらいは行くのでは・・・🤔

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/3153c189-c6ad-2189-ce22-b9d0b2287256.png)


##全体のソース##

<details><summary>NimbleTest.ino</summary>
<div>

```cpp:NimbleTest.ino

/** NimBLE_Server Demo:
 *
 *  Demonstrates many of the available features of the NimBLE client library.
 *  
 *  Created: on March 24 2020
 *      Author: H2zero
 * 
 */

#include <NimBLEDevice.h>
#include <M5StickC.h>


#include <vector>
using namespace std;

void scanEndedCB(NimBLEScanResults results);

// UUID HID
static NimBLEUUID serviceUUID("1812");
// UUID Report Charcteristic
static NimBLEUUID charUUID("2a4d");

static NimBLEAdvertisedDevice *advDevice;

static bool doConnect = false;
static uint32_t scanTime = 0; /** 0 = scan forever */


class ClientCallbacks: public NimBLEClientCallbacks {
  void onConnect(NimBLEClient *pClient) {
    Serial.println("Connected");
    /** After connection we should change the parameters if we don't need fast response times.
     *  These settings are 150ms interval, 0 latency, 450ms timout.
     *  Timeout should be a multiple of the interval, minimum is 100ms.
     *  I find a multiple of 3-5 * the interval works best for quick response/reconnect.
     *  Min interval: 120 * 1.25ms = 150, Max interval: 120 * 1.25ms = 150, 0 latency, 60 * 10ms = 600ms timeout
     */
    pClient->updateConnParams(120, 120, 0, 60);
  }
  ;

  void onDisconnect(NimBLEClient *pClient) {
    Serial.print(pClient->getPeerAddress().toString().c_str());
    Serial.println(" Disconnected - Starting scan");
    NimBLEDevice::getScan()->start(scanTime, scanEndedCB);
  }
  ;

  /** Called when the peripheral requests a change to the connection parameters.
   *  Return true to accept and apply them or false to reject and keep
   *  the currently used parameters. Default will return true.
   */
  bool onConnParamsUpdateRequest(NimBLEClient *pClient, const ble_gap_upd_params *params) {
    if (params->itvl_min < 24) { /** 1.25ms units */
      return false;
    }
    else if (params->itvl_max > 40) { /** 1.25ms units */
      return false;
    }
    else if (params->latency > 2) { /** Number of intervals allowed to skip */
      return false;
    }
    else if (params->supervision_timeout > 100) { /** 10ms units */
      return false;
    }

    return true;
  }
  ;

  /********************* Security handled here **********************
   ****** Note: these are the same return values as defaults ********/
  uint32_t onPassKeyRequest() {
    Serial.println("Client Passkey Request");
    /** return the passkey to send to the server */
    return 123456;
  }
  ;

  bool onConfirmPIN(uint32_t pass_key) {
    Serial.print("The passkey YES/NO number: ");
    Serial.println(pass_key);
    /** Return false if passkeys don't match. */
    return true;
  }
  ;

  /** Pairing proces\s complete, we can check the results in ble_gap_conn_desc */
  void onAuthenticationComplete(ble_gap_conn_desc *desc) {
    if (!desc->sec_state.encrypted) {
      Serial.println("Encrypt connection failed - disconnecting");
      /** Find the client with the connection handle provided in desc */
      NimBLEDevice::getClientByID(desc->conn_handle)->disconnect();
      return;
    }
  }
  ;
};

/** Define a class to handle the callbacks when advertisments are received */
class AdvertisedDeviceCallbacks: public NimBLEAdvertisedDeviceCallbacks {

  void onResult(NimBLEAdvertisedDevice *advertisedDevice) {
    Serial.print("Advertised Device found: ");
//        Serial.println(advertisedDevice->toString().c_str());
    Serial.printf("name:%s, address:%s ", advertisedDevice->getName().c_str(), advertisedDevice->getAddress().toString().c_str());
    Serial.printf("UUID:%s\n", advertisedDevice->haveServiceUUID() ? advertisedDevice->getServiceUUID().toString().c_str() : "none");

    if (advertisedDevice->isAdvertisingService(serviceUUID)) {
      Serial.println("Found Our Service");
      /** stop scan before connecting */
      NimBLEDevice::getScan()->stop();
      /** Save the device reference in a global for the client to use*/
      advDevice = advertisedDevice;
      /** Ready to connect now */
      doConnect = true;
    }
  }
  ;
};

const uint8_t START = 0x08;
const uint8_t SELECT = 0x04;

const int INDEX_BUTTON = 6;

bool on = false;

/** Notification / Indication receiving handler callback */
void notifyCB(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
  std::string str = (isNotify == true) ? "Notification" : "Indication";
  str += " from ";
  str += pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress().toString();
  str += ": Service = " + pRemoteCharacteristic->getRemoteService()->getUUID().toString();
  str += ", Characteristic = " + pRemoteCharacteristic->getUUID().toString();
//    str += ", Value = " + std::string((char*)pData, length);
  Serial.print(str.c_str());
  Serial.print("\ndata: ");
  for (int i = 0; i < length; i++) {
    // uint8_tを頭0のstringで表示する
    Serial.printf("%02X ", pData[i]);
  }
  Serial.print("\n");
  if (pData[1] & 0x28) {
    digitalWrite(GPIO_NUM_10, LOW);
    on = true;
  }
  if (pData[0] & 0x01) {
    if (on) {
    on = false;
    return;
  }
    digitalWrite(GPIO_NUM_10, HIGH);
  }

}

/** Callback to process the results of the last scan or restart it */
void scanEndedCB(NimBLEScanResults results) {
  Serial.println("Scan Ended");
}

/** Create a single global instance of the callback class to be used by all clients */
static ClientCallbacks clientCB;

/** Handles the provisioning of clients and connects / interfaces with the server */
bool connectToServer() {
  NimBLEClient *pClient = nullptr;

  /** Check if we have a client we should reuse first **/
  if (NimBLEDevice::getClientListSize()) {
    /** Special case when we already know this device, we send false as the
     *  second argument in connect() to prevent refreshing the service database.
     *  This saves considerable time and power.
     */
    pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
    if (pClient) {
      if (!pClient->connect(advDevice, false)) {
        Serial.println("Reconnect failed");
        return false;
      }
      Serial.println("Reconnected client");
    }
    /** We don't already have a client that knows this device,
     *  we will check for a client that is disconnected that we can use.
     */
    else {
      pClient = NimBLEDevice::getDisconnectedClient();
    }
  }

  /** No client to reuse? Create a new one. */
  if (!pClient) {
    if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
      Serial.println("Max clients reached - no more connections available");
      return false;
    }

    pClient = NimBLEDevice::createClient();

    Serial.println("New client created");

    pClient->setClientCallbacks(&clientCB, false);
    /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout.
     *  These settings are safe for 3 clients to connect reliably, can go faster if you have less
     *  connections. Timeout should be a multiple of the interval, minimum is 100ms.
     *  Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout
     */
    pClient->setConnectionParams(12, 12, 0, 51);
    /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */
    pClient->setConnectTimeout(5);

    if (!pClient->connect(advDevice)) {
      /** Created a client but failed to connect, don't need to keep it as it has no data */
      NimBLEDevice::deleteClient(pClient);
      Serial.println("Failed to connect, deleted client");
      return false;
    }
  }

  if (!pClient->isConnected()) {
    if (!pClient->connect(advDevice)) {
      Serial.println("Failed to connect");
      return false;
    }
  }

  Serial.print("Connected to: ");
  Serial.println(pClient->getPeerAddress().toString().c_str());
  Serial.print("RSSI: ");
  Serial.println(pClient->getRssi());

  /** Now we can read/write/subscribe the charateristics of the services we are interested in */
  NimBLERemoteService *pSvc = nullptr;
//  NimBLERemoteCharacteristic *pChr = nullptr;
  std::vector<NimBLERemoteCharacteristic*> *pChrs = nullptr;

  NimBLERemoteDescriptor *pDsc = nullptr;

  pSvc = pClient->getService(serviceUUID);
  if (pSvc) { /** make sure it's not null */
    pChrs = pSvc->getCharacteristics(true);
  }

  if (pChrs) { /** make sure it's not null */

    for (int i = 0; i < pChrs->size(); i++) {

      if (pChrs->at(i)->canNotify()) {
        /** Must send a callback to subscribe, if nullptr it will unsubscribe */
        if (!pChrs->at(i)->registerForNotify(notifyCB)) {
          /** Disconnect if subscribe failed */
          pClient->disconnect();
          return false;
        }
      }
    }
  }

  else {
    Serial.println("DEAD service not found.");
  }

  Serial.println("Done with this device!");
  return true;
}

void setup() {
  Serial.begin(115200);
  Serial.println("Starting NimBLE Client");
  M5.begin();
  pinMode(GPIO_NUM_10, OUTPUT);
  digitalWrite(GPIO_NUM_10, HIGH);

  /** Initialize NimBLE, no device name spcified as we are not advertising */
  NimBLEDevice::init("");

  /** Set the IO capabilities of the device, each option will trigger a different pairing method.
   *  BLE_HS_IO_KEYBOARD_ONLY    - Passkey pairing
   *  BLE_HS_IO_DISPLAY_YESNO   - Numeric comparison pairing
   *  BLE_HS_IO_NO_INPUT_OUTPUT - DEFAULT setting - just works pairing
   */
//NimBLEDevice::setSecurityIOCap(BLE_HS_IO_KEYBOARD_ONLY); // use passkey
//NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_YESNO); //use numeric comparison
  /** 2 different ways to set security - both calls achieve the same result.
   *  no bonding, no man in the middle protection, secure connections.
   *
   *  These are the default values, only shown here for demonstration.
   */
//NimBLEDevice::setSecurityAuth(false, false, true);
  NimBLEDevice::setSecurityAuth(/*BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM |*/BLE_SM_PAIR_AUTHREQ_SC);

  /** Optional: set the transmit power, default is 3db */
  NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */

  /** Optional: set any devices you don't want to get advertisments from */
// NimBLEDevice::addIgnored(NimBLEAddress ("aa:bb:cc:dd:ee:ff"));
  /** create new scan */
  NimBLEScan *pScan = NimBLEDevice::getScan();

  /** create a callback that gets called when advertisers are found */
  pScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks());

  /** Set scan interval (how often) and window (how long) in milliseconds */
  pScan->setInterval(10000);
  pScan->setWindow(9999);

  /** Active scan will gather scan response data from advertisers
   *  but will use more energy from both devices
   */
  pScan->setActiveScan(true);
  /** Start scanning for advertisers for the scan time specified (in seconds) 0 = forever
   *  Optional callback for when scanning stops.
   */
  pScan->start(scanTime, scanEndedCB);
}

void loop() {
  M5.update();
  if (M5.BtnA.wasReleased()) {
    M5.Axp.PowerOff();
  }
    if (doConnect) {
        if (connectToServer()) {
            Serial.println("Success! we should now be getting notifications, scanning for more!");
        }
        else {
            Serial.println("Failed to connect, starting scan");
        }
        doConnect = false;
    }
    /** Found a device we want to connect to, do it now */
    delay(1);
}
```
</div>
</details>

##複数デバイスに対応する##
現在の実装ではHIDのUUIDが見つかった時点でスキャンを停止しており
単一のデバイスしか接続できないようになっています

なのでデバイスが一つ見つかってもスキャンを止めないようにします
また、見つかったデバイスを複数保存できるようにvectorを使用します

```cpp
static NimBLEAdvertisedDevice *advDevice;

// vectorを追加
std::vector<NimBLEAdvertisedDevice*> advDevices;

class AdvertisedDeviceCallbacks: public NimBLEAdvertisedDeviceCallbacks {

    void onResult(NimBLEAdvertisedDevice *advertisedDevice) {
        Serial.print("Advertised Device found: ");
//        Serial.println(advertisedDevice->toString().c_str());
        Serial.printf("name:%s,address:%s", advertisedDevice->getName().c_str(), advertisedDevice->getAddress().toString().c_str());
        Serial.printf("UUID:%s\n", advertisedDevice->haveServiceUUID() ? advertisedDevice->getServiceUUID().toString().c_str() : "none");

        if (advertisedDevice->isAdvertisingService(serviceUUID)) {
            Serial.println("Found Our Service");
            //スキャンを止めるな！
            //      NimBLEDevice::getScan()->stop();
            /** Save the device reference in a global for the client to use*/
            // HIDデバイスが登録されてなければ登録
            if (advDevices.size() == 0) {
                advDevice = advertisedDevice;
                // vectorに追加
                advDevices.push_back(advertisedDevice);
                Serial.printf("onResult:myDevices=%d\n", advDevices.size());
            }
            else {
                for (int i = 0; i < advDevices.size(); i++) {
                    // すでに登録されていれば重複チェック
                    if (advDevices.at(i)->getAddress().equals(advertisedDevice->getAddress())) {
                        Serial.printf("onResult:device already added\n");
                        // 重複していたらreturn
                        return;
                    }
                }
                // 重複がなければvectorに登録
                advDevice = advertisedDevice;
                advDevices.push_back(advertisedDevice);
                Serial.printf("onResult:myDevices=%d\n", advDevices.size());
            }

        }
    }
    ;
};

```

```cpp

bool connectToServer() {
    NimBLEClient *pClient = nullptr;
    // vectorに存在するHIDデバイスの数だけNotify登録を行う
    Serial.printf("myDevices=%d\n", advDevices.size());
    for (int i = 0; i < advDevices.size(); i++) {
        if (NimBLEDevice::getClientListSize()) {
            pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
            if (pClient) {
                if (!pClient->connect(advDevices.at(i), false)) {
                    Serial.println("Reconnect failed");
                    return false;
                }
                Serial.println("Reconnected client");
            }
            /** We don't already have a client that knows this device,
             *  we will check for a client that is disconnected that we can use.
             */
            else {
                pClient = NimBLEDevice::getDisconnectedClient();
            }
        }

        /** No client to reuse? Create a new one. */
        if (!pClient) {
            if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
                Serial.println("Max clients reached - no more connections available");
                return false;
            }

            pClient = NimBLEDevice::createClient();

            Serial.println("New client created");

            pClient->setClientCallbacks(&clientCB, false);
            pClient->setConnectionParams(12, 12, 0, 51);
            /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */
            pClient->setConnectTimeout(5);

            if (!pClient->connect(advDevices.at(i))) {
                /** Created a client but failed to connect, don't need to keep it as it has no data */
                NimBLEDevice::deleteClient(pClient);
                Serial.println("Failed to connect, deleted client");
                advDevices.clear();
                return false;
            }
        }

        if (!pClient->isConnected()) {
            if (!pClient->connect(advDevices.at(i))) {
                Serial.println("Failed to connect");
                return false;
            }
        }

        Serial.print("Connected to: ");
        Serial.println(pClient->getPeerAddress().toString().c_str());
        Serial.print("RSSI: ");
        Serial.println(pClient->getRssi());

        /** Now we can read/write/subscribe the charateristics of the services we are interested in */
        NimBLERemoteService *pSvc = nullptr;
        //  NimBLERemoteCharacteristic *pChr = nullptr;
        std::vector<NimBLERemoteCharacteristic*> *pChrs = nullptr;

        pSvc = pClient->getService(serviceUUID);
        if (pSvc) { /** make sure it's not null */
            pChrs = pSvc->getCharacteristics(true);
        }

        if (pChrs) { /** make sure it's not null */

            for (int i = 0; i < pChrs->size(); i++) {

                if (pChrs->at(i)->canNotify()) {
                    /** Must send a callback to subscribe, if nullptr it will unsubscribe */
                    if (!pChrs->at(i)->registerForNotify(notifyCB)) {
                        /** Disconnect if subscribe failed */
                        pClient->disconnect();
                        return false;
                    }
                }
            }
        }

        else {
            Serial.println("HID service not found.");
        }
    }
    advDevices.clear();
    Serial.println("Done with this device!");
    return true;
}
```

こうすることでHIDデバイスが複数存在しても接続することができます

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ヤッター<a href="https://twitter.com/hashtag/NimBLE?src=hash&amp;ref_src=twsrc%5Etfw">#NimBLE</a> でBLE HIDデバイス(ダイソーシャッター)3台接続できたよー＼(^o^)／<a href="https://twitter.com/hashtag/M5StickC?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickC</a> <a href="https://t.co/3912KDYsLm">pic.twitter.com/3912KDYsLm</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1302300693303181312?ref_src=twsrc%5Etfw">September 5, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<details><summary>NimbleHidMulti.ino</summary>
<div>

```cpp:NimbleHidMulti.ino

/** NimBLE_Server Demo:
 *
 *  Demonstrates many of the available features of the NimBLE client library.
 *  
 *  Created: on March 24 2020
 *      Author: H2zero
 * 
 */

#include <NimBLEDevice.h>
#include <M5StickC.h>

void scanEndedCB(NimBLEScanResults results);

// UUID HID
static NimBLEUUID serviceUUID("1812");
// UUID Report Charcteristic
static NimBLEUUID charUUID("2a4d");

static NimBLEAdvertisedDevice *advDevice;

std::vector<NimBLEAdvertisedDevice*> advDevices;

static bool doConnect = false;
static uint32_t scanTime = 1; /** 0 = scan forever */

/**  None of these are required as they will be handled by the library with defaults. **
 **                       Remove as you see fit for your needs                        */
class ClientCallbacks: public NimBLEClientCallbacks {
    void onConnect(NimBLEClient *pClient) {
        Serial.println("Connected");
        /** After connection we should change the parameters if we don't need fast response times.
         *  These settings are 150ms interval, 0 latency, 450ms timout.
         *  Timeout should be a multiple of the interval, minimum is 100ms.
         *  I find a multiple of 3-5 * the interval works best for quick response/reconnect.
         *  Min interval: 120 * 1.25ms = 150, Max interval: 120 * 1.25ms = 150, 0 latency, 60 * 10ms = 600ms timeout
         */
        pClient->updateConnParams(120, 120, 0, 60);
    }
    ;

    void onDisconnect(NimBLEClient *pClient) {
        Serial.print(pClient->getPeerAddress().toString().c_str());
        Serial.println(" Disconnected - Starting scan");
        NimBLEDevice::getScan()->start(scanTime, scanEndedCB);
    }
    ;

    /** Called when the peripheral requests a change to the connection parameters.
     *  Return true to accept and apply them or false to reject and keep
     *  the currently used parameters. Default will return true.
     */
    bool onConnParamsUpdateRequest(NimBLEClient *pClient, const ble_gap_upd_params *params) {
        if (params->itvl_min < 24) { /** 1.25ms units */
            return false;
        }
        else if (params->itvl_max > 40) { /** 1.25ms units */
            return false;
        }
        else if (params->latency > 2) { /** Number of intervals allowed to skip */
            return false;
        }
        else if (params->supervision_timeout > 100) { /** 10ms units */
            return false;
        }

        return true;
    }
    ;

    /********************* Security handled here **********************
     ****** Note: these are the same return values as defaults ********/
    uint32_t onPassKeyRequest() {
        Serial.println("Client Passkey Request");
        /** return the passkey to send to the server */
        return 123456;
    }
    ;

    bool onConfirmPIN(uint32_t pass_key) {
        Serial.print("The passkey YES/NO number: ");
        Serial.println(pass_key);
        /** Return false if passkeys don't match. */
        return true;
    }
    ;

    /** Pairing proces\s complete, we can check the results in ble_gap_conn_desc */
    void onAuthenticationComplete(ble_gap_conn_desc *desc) {
        if (!desc->sec_state.encrypted) {
            Serial.println("Encrypt connection failed - disconnecting");
            /** Find the client with the connection handle provided in desc */
            NimBLEDevice::getClientByID(desc->conn_handle)->disconnect();
            return;
        }
    }
    ;
};

/** Define a class to handle the callbacks when advertisments are received */
class AdvertisedDeviceCallbacks: public NimBLEAdvertisedDeviceCallbacks {

    void onResult(NimBLEAdvertisedDevice *advertisedDevice) {
        Serial.print("Advertised Device found: ");
//        Serial.println(advertisedDevice->toString().c_str());
        Serial.printf("name:%s,address:%s", advertisedDevice->getName().c_str(), advertisedDevice->getAddress().toString().c_str());
        Serial.printf("UUID:%s\n", advertisedDevice->haveServiceUUID() ? advertisedDevice->getServiceUUID().toString().c_str() : "none");

        if (advertisedDevice->isAdvertisingService(serviceUUID)) {
            Serial.println("Found Our Service");
            /** stop scan before connecting */
//      NimBLEDevice::getScan()->stop();
            /** Save the device reference in a global for the client to use*/

            if (advDevices.size() == 0) {
                advDevice = advertisedDevice;
                advDevices.push_back(advertisedDevice);
                Serial.printf("onResult:myDevices=%d\n", advDevices.size());
            }
            else {
                for (int i = 0; i < advDevices.size(); i++) {
                    if (advDevices.at(i)->getAddress().equals(advertisedDevice->getAddress())) {
                        Serial.printf("onResult:device already added\n");
                        return;
                    }
                }
                advDevice = advertisedDevice;
                advDevices.push_back(advertisedDevice);
                Serial.printf("onResult:myDevices=%d\n", advDevices.size());
            }

        }
    }
    ;
};

const uint8_t START = 0x08;
const uint8_t SELECT = 0x04;

const int INDEX_BUTTON = 6;

bool on = false;

/** Notification / Indication receiving handler callback */
void notifyCB(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) {
    std::string str = (isNotify == true) ? "Notification" : "Indication";
    str += " from ";
    str += pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress().toString();
    str += ": Service = " + pRemoteCharacteristic->getRemoteService()->getUUID().toString();
    str += ", Characteristic = " + pRemoteCharacteristic->getUUID().toString();
//    str += ", Value = " + std::string((char*)pData, length);
    Serial.print(str.c_str());
    Serial.print("\ndata: ");
    for (int i = 0; i < length; i++) {
        // uint8_tを頭0のstringで表示する
        Serial.printf("%02X ", pData[i]);
    }
    Serial.print("\n");
    if (pData[1] & 0x28) {
        digitalWrite(GPIO_NUM_10, LOW);
        on = true;
    }
    if (pData[0] & 0x01) {
        if (on) {
            on = false;
            return;
        }
        digitalWrite(GPIO_NUM_10, HIGH);
    }

}

/** Callback to process the results of the last scan or restart it */
void scanEndedCB(NimBLEScanResults results) {
    Serial.println("Scan Ended");
    if (advDevices.size() != 0 && !doConnect) {
//        NimBLEDevice::getScan()->stop();
        doConnect = true;
    }
}

/** Create a single global instance of the callback class to be used by all clients */
static ClientCallbacks clientCB;

/** Handles the provisioning of clients and connects / interfaces with the server */
bool connectToServer() {
    NimBLEClient *pClient = nullptr;

    Serial.printf("myDevices=%d\n", advDevices.size());
    for (int i = 0; i < advDevices.size(); i++) {
        /** Check if we have a client we should reuse first **/
        if (NimBLEDevice::getClientListSize()) {
            /** Special case when we already know this device, we send false as the
             *  second argument in connect() to prevent refreshing the service database.
             *  This saves considerable time and power.
             */
            pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
            if (pClient) {
                if (!pClient->connect(advDevices.at(i), false)) {
                    Serial.println("Reconnect failed");
                    return false;
                }
                Serial.println("Reconnected client");
            }
            /** We don't already have a client that knows this device,
             *  we will check for a client that is disconnected that we can use.
             */
            else {
                pClient = NimBLEDevice::getDisconnectedClient();
            }
        }

        /** No client to reuse? Create a new one. */
        if (!pClient) {
            if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
                Serial.println("Max clients reached - no more connections available");
                return false;
            }

            pClient = NimBLEDevice::createClient();

            Serial.println("New client created");

            pClient->setClientCallbacks(&clientCB, false);
            /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout.
             *  These settings are safe for 3 clients to connect reliably, can go faster if you have less
             *  connections. Timeout should be a multiple of the interval, minimum is 100ms.
             *  Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout
             */
            pClient->setConnectionParams(12, 12, 0, 51);
            /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */
            pClient->setConnectTimeout(5);

            if (!pClient->connect(advDevices.at(i))) {
                /** Created a client but failed to connect, don't need to keep it as it has no data */
                NimBLEDevice::deleteClient(pClient);
                Serial.println("Failed to connect, deleted client");
                advDevices.clear();
                return false;
            }
        }

        if (!pClient->isConnected()) {
            if (!pClient->connect(advDevices.at(i))) {
                Serial.println("Failed to connect");
                return false;
            }
        }

        Serial.print("Connected to: ");
        Serial.println(pClient->getPeerAddress().toString().c_str());
        Serial.print("RSSI: ");
        Serial.println(pClient->getRssi());

        /** Now we can read/write/subscribe the charateristics of the services we are interested in */
        NimBLERemoteService *pSvc = nullptr;
        //  NimBLERemoteCharacteristic *pChr = nullptr;
        std::vector<NimBLERemoteCharacteristic*> *pChrs = nullptr;

//    NimBLERemoteDescriptor *pDsc = nullptr;

        pSvc = pClient->getService(serviceUUID);
        if (pSvc) { /** make sure it's not null */
            pChrs = pSvc->getCharacteristics(true);
        }

        if (pChrs) { /** make sure it's not null */

            for (int i = 0; i < pChrs->size(); i++) {

                if (pChrs->at(i)->canNotify()) {
                    /** Must send a callback to subscribe, if nullptr it will unsubscribe */
                    if (!pChrs->at(i)->registerForNotify(notifyCB)) {
                        /** Disconnect if subscribe failed */
                        pClient->disconnect();
                        return false;
                    }
                }
            }
        }

        else {
            Serial.println("HID service not found.");
        }
    }
    advDevices.clear();
    Serial.println("Done with this device!");
    return true;
}

void setup() {
    Serial.begin(115200);
    Serial.println("Starting NimBLE Client");
    M5.begin();
    pinMode(GPIO_NUM_10, OUTPUT);
    digitalWrite(GPIO_NUM_10, HIGH);

    /** Initialize NimBLE, no device name spcified as we are not advertising */
    NimBLEDevice::init("");

    /** Set the IO capabilities of the device, each option will trigger a different pairing method.
     *  BLE_HS_IO_KEYBOARD_ONLY    - Passkey pairing
     *  BLE_HS_IO_DISPLAY_YESNO   - Numeric comparison pairing
     *  BLE_HS_IO_NO_INPUT_OUTPUT - DEFAULT setting - just works pairing
     */
//NimBLEDevice::setSecurityIOCap(BLE_HS_IO_KEYBOARD_ONLY); // use passkey
//NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_YESNO); //use numeric comparison
    /** 2 different ways to set security - both calls achieve the same result.
     *  no bonding, no man in the middle protection, secure connections.
     *
     *  These are the default values, only shown here for demonstration.
     */
//NimBLEDevice::setSecurityAuth(false, false, true);
    NimBLEDevice::setSecurityAuth(/*BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM |*/BLE_SM_PAIR_AUTHREQ_SC);

    /** Optional: set the transmit power, default is 3db */
    NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */

    /** Optional: set any devices you don't want to get advertisments from */
// NimBLEDevice::addIgnored(NimBLEAddress ("aa:bb:cc:dd:ee:ff"));
    /** create new scan */
    NimBLEScan *pScan = NimBLEDevice::getScan();

    /** create a callback that gets called when advertisers are found */
    pScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks());

    /** Set scan interval (how often) and window (how long) in milliseconds */
    pScan->setInterval(10000);
    pScan->setWindow(9999);

    /** Active scan will gather scan response data from advertisers
     *  but will use more energy from both devices
     */
    pScan->setActiveScan(true);
    /** Start scanning for advertisers for the scan time specified (in seconds) 0 = forever
     *  Optional callback for when scanning stops.
     */
    pScan->start(scanTime, scanEndedCB, false);
}

void loop() {
    M5.update();
    if (M5.BtnA.wasReleased()) {
        M5.Axp.PowerOff();
    }
    if (M5.BtnB.wasReleased()) {
        ESP.restart();
    }
    if (doConnect) {
        if (connectToServer()) {
            Serial.println("Success! we should now be getting notifications, scanning for more!");
        }
        else {
            Serial.println("Failed to connect, starting scan");
        }
        doConnect = false;
    }
    /** Found a device we want to connect to, do it now */
    delay(1);
}

```
</div>
</details>

##bCore4(HIDではないデバイス)に接続する##

bCore4と言う汎用多目的超小型BLEリモコンロボットコアデバイスを操作します

bCore4と言うのはこういうものです

bCore4 - スイッチサイエンス
https://www.switch-science.com/catalog/8142/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/c241a9a2-2fba-d4e0-9b76-19c645827cd5.png)

bCore4は、汎用多目的超小型BLEリモコンロボットコアデバイス bCoreシリーズの第四世代です。500円玉よりも小さい超小型でありながら、4つのサーボ、4つのLED、2つのモーターを電池2〜4本でiOS/Androd/Windowsアプリから動かすことができます。 技適対応で日本国内で法令に則って利用が可能です

スマホから操作できるし公式のサンプルもあるので
スケッチの容量がギリギリとかじゃないとNimBLE使う意味もないかと思いますが
忘備録代わりという事で・・・(´・ω・`)

まずbCore4のドキュメントを確認します

BLE関連はここになります

2.1._bCoreServiceCharacteristic · ymmtynk/bCore Wiki
https://github.com/ymmtynk/bCore/wiki/2.1._bCoreServiceCharacteristic

使用するUUIDを列挙します

```cpp

// UUID bCore Service
static NimBLEUUID bCoreUUID("389CAAF0-843F-4d3b-959D-C954CCE14655");
// UUID getBattery Charcteristic
static NimBLEUUID getBatteryUUID("389CAAF1-843F-4d3b-959D-C954CCE14655");
// UUID burstComand Charcteristic
static NimBLEUUID burstCommandUUID("389CAAF5-843F-4d3b-959D-C954CCE14655");
// UUID getFunctions Charcteristic
static NimBLEUUID getFunctionUUID("389CAAFF-843F-4d3b-959D-C954CCE14655");

NimBLERemoteCharacteristic *burstCommandCharacteristic;
NimBLERemoteCharacteristic *getBatteryCharacteristic;

```

NimBLEAdvertisedDeviceCallbacks.onResult()内をbCore4のUUIDを使用するように変更します

```cpp
class AdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks {

  void onResult(NimBLEAdvertisedDevice *advertisedDevice) {
    Serial.print("Advertised Device found: ");
    //        Serial.println(advertisedDevice->toString().c_str());
    Serial.printf("name:%s, address:%s ", advertisedDevice->getName().c_str(),
                  advertisedDevice->getAddress().toString().c_str());
    Serial.printf("UUID:%s\n",
                  advertisedDevice->haveServiceUUID()
                      ? advertisedDevice->getServiceUUID().toString().c_str()
                      : "none");

// bCoreUUIDを使用する
    if (advertisedDevice->isAdvertisingService(bCoreUUID)) {
      Serial.println("Found Our Service");
      /** stop scan before connecting */
      NimBLEDevice::getScan()->stop();
      /** Save the device reference in a global for the client to use*/
      advDevice = advertisedDevice;
      /** Ready to connect now */
      doConnect = true;
    }
  };
};
```

connectToServer()内のCharacteristicsを取得する部分をbCore4のUUIDに変更します

```cpp
// 該当部を抜粋

  NimBLERemoteService *pSvc = nullptr;
  //  NimBLERemoteCharacteristic *pChr = nullptr;
  std::vector<NimBLERemoteCharacteristic *> *pChrs = nullptr;

  NimBLERemoteDescriptor *pDsc = nullptr;

//  bCore Serviceを取得する
  pSvc = pClient->getService(bCoreUUID);
  if (pSvc) { /** make sure it's not null */
    // Characteristicsのvectorを取得する
    pChrs = pSvc->getCharacteristics(true);
  }

  if (pChrs) { /** make sure it's not null */

    for (int i = 0; i < pChrs->size(); i++) {
      if (pChrs->at(i)->getUUID().equals(getFunctionUUID)) {
        // getFunctions Charcteristicなら内容を表示
        Serial.println("getFunctionChars found.");
        Serial.printf("data:%02X \n", pChrs->at(i)->readUInt16());
      }
      else if (pChrs->at(i)->getUUID().equals(burstCommandUUID)) {
        // burstCommandCharacteristicを取得する
        Serial.println("burstCommandChars found.");
        burstCommandCharacteristic = pChrs->at(i);
      }
      else if (pChrs->at(i)->getUUID().equals(getBatteryUUID)) {
        // getBattery Charcteristicを取得する
        Serial.println("getBatteryChars found.");
        getBatteryCharacteristic = pChrs->at(i);
      }
    }
  }

```

burstCommandCharacteristicが取得出来たらコマンドを送ることでbCore4が操作できます

```cpp

byte burstCommandBuffer[7];


// モーターを操作するので[0],[1]のみ設定してその他はすべて出力無し
void sendBurstCommand(uint8_t ch0, uint8_t ch1) {
  burstCommandBuffer[0] = ch0;
  burstCommandBuffer[1] = ch1;
  burstCommandBuffer[2] = 0x80;
  burstCommandBuffer[3] = 0x80;
  burstCommandBuffer[4] = 0x80;
  burstCommandBuffer[5] = 0x80;
  burstCommandBuffer[6] = 0x80;
  burstCommandCharacteristic->writeValue(burstCommandBuffer);
}

// 使用例
// 停止
sendBurstCommand(0x80, 0x80);
// 前進
sendBurstCommand(0xFF, 0xFF);
// 後退
sendBurstCommand(0x00, 0x00);


```

writeValue()でコマンドを送るとbCore4を操作することができます

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/bCore4?src=hash&amp;ref_src=twsrc%5Etfw">#bCore4</a> と <a href="https://twitter.com/hashtag/M5StickC?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickC</a> のフェーダーUnitでモタチカ出来ました＼(^o^)／<a href="https://twitter.com/hashtag/%E3%83%A2%E3%82%BF%E3%83%81%E3%82%AB?src=hash&amp;ref_src=twsrc%5Etfw">#モタチカ</a> <a href="https://twitter.com/hashtag/%E3%81%A8%E3%81%AF?src=hash&amp;ref_src=twsrc%5Etfw">#とは</a> <a href="https://t.co/fU2TobRvl1">pic.twitter.com/fU2TobRvl1</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1533402363335438336?ref_src=twsrc%5Etfw">June 5, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<details><summary>main.cpp</summary>
<div>

```cpp:main.cpp
#include <Arduino.h>
#include <M5StickC.h>
#include <NimBLEDevice.h>

#include <vector>
using namespace std;
#include "porthub.h"
void scanEndedCB(NimBLEScanResults results);

// UUID bCore Service
static NimBLEUUID bCoreUUID("389CAAF0-843F-4d3b-959D-C954CCE14655");
// UUID getBattery Charcteristic
static NimBLEUUID getBatteryUUID("389CAAF1-843F-4d3b-959D-C954CCE14655");
// UUID burstComand Charcteristic
static NimBLEUUID burstCommandUUID("389CAAF5-843F-4d3b-959D-C954CCE14655");
// UUID getFunctions Charcteristic
static NimBLEUUID getFunctionUUID("389CAAFF-843F-4d3b-959D-C954CCE14655");

static NimBLEAdvertisedDevice *advDevice;

NimBLERemoteCharacteristic *burstCommandCharacteristic;
NimBLERemoteCharacteristic *getBatteryCharacteristic;

static bool doConnect = false;
static uint32_t scanTime = 0; /** 0 = scan forever */

class ClientCallbacks : public NimBLEClientCallbacks {
  void onConnect(NimBLEClient *pClient) {
    Serial.println("Connected");
    /** After connection we should change the parameters if we don't need fast
     * response times. These settings are 150ms interval, 0 latency, 450ms
     * timout. Timeout should be a multiple of the interval, minimum is 100ms.
     *  I find a multiple of 3-5 * the interval works best for quick
     * response/reconnect. Min interval: 120 * 1.25ms = 150, Max interval: 120
     * * 1.25ms = 150, 0 latency, 60 * 10ms = 600ms timeout
     */
    pClient->updateConnParams(120, 120, 0, 60);
  };

  void onDisconnect(NimBLEClient *pClient) {
    Serial.print(pClient->getPeerAddress().toString().c_str());
    Serial.println(" Disconnected - Starting scan");
    ESP.restart();
    NimBLEDevice::getScan()->start(scanTime, scanEndedCB);
  };

  /** Called when the peripheral requests a change to the connection parameters.
   *  Return true to accept and apply them or false to reject and keep
   *  the currently used parameters. Default will return true.
   */
  bool onConnParamsUpdateRequest(NimBLEClient *pClient,
                                 const ble_gap_upd_params *params) {
    if (params->itvl_min < 24) { /** 1.25ms units */
      return false;
    }
    else if (params->itvl_max > 40) { /** 1.25ms units */
      return false;
    }
    else if (params->latency > 2) { /** Number of intervals allowed to skip */
      return false;
    }
    else if (params->supervision_timeout > 100) { /** 10ms units */
      return false;
    }

    return true;
  };

  /********************* Security handled here **********************
   ****** Note: these are the same return values as defaults ********/
  uint32_t onPassKeyRequest() {
    Serial.println("Client Passkey Request");
    /** return the passkey to send to the server */
    return 123456;
  };

  bool onConfirmPIN(uint32_t pass_key) {
    Serial.print("The passkey YES/NO number: ");
    Serial.println(pass_key);
    /** Return false if passkeys don't match. */
    return true;
  };

  /** Pairing proces\s complete, we can check the results in ble_gap_conn_desc
   */
  void onAuthenticationComplete(ble_gap_conn_desc *desc) {
    if (!desc->sec_state.encrypted) {
      Serial.println("Encrypt connection failed - disconnecting");
      /** Find the client with the connection handle provided in desc */
      NimBLEDevice::getClientByID(desc->conn_handle)->disconnect();
      return;
    }
  };
};

/** Define a class to handle the callbacks when advertisments are received */
class AdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks {

  void onResult(NimBLEAdvertisedDevice *advertisedDevice) {
    Serial.print("Advertised Device found: ");
    //        Serial.println(advertisedDevice->toString().c_str());
    Serial.printf("name:%s, address:%s ", advertisedDevice->getName().c_str(),
                  advertisedDevice->getAddress().toString().c_str());
    Serial.printf("UUID:%s\n",
                  advertisedDevice->haveServiceUUID()
                      ? advertisedDevice->getServiceUUID().toString().c_str()
                      : "none");

    if (advertisedDevice->isAdvertisingService(bCoreUUID)) {
      Serial.println("Found Our Service");
      /** stop scan before connecting */
      NimBLEDevice::getScan()->stop();
      /** Save the device reference in a global for the client to use*/
      advDevice = advertisedDevice;
      /** Ready to connect now */
      doConnect = true;
    }
  };
};

const uint8_t START = 0x08;
const uint8_t SELECT = 0x04;

const int INDEX_BUTTON = 6;

bool on = false;

/** Notification / Indication receiving handler callback */
void notifyCB(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData,
              size_t length, bool isNotify) {
  std::string str = (isNotify == true) ? "Notification" : "Indication";
  str += " from ";
  str += pRemoteCharacteristic->getRemoteService()
             ->getClient()
             ->getPeerAddress()
             .toString();
  str += ": Service = " +
         pRemoteCharacteristic->getRemoteService()->getUUID().toString();
  str += ", Characteristic = " + pRemoteCharacteristic->getUUID().toString();
  //    str += ", Value = " + std::string((char*)pData, length);
  Serial.print(str.c_str());
  Serial.print("\ndata: ");
  for (int i = 0; i < length; i++) {
    // uint8_tを頭0のstringで表示する
    Serial.printf("%02X ", pData[i]);
  }
  Serial.print("\n");
  if (pData[1] & 0x28) {
    digitalWrite(GPIO_NUM_10, LOW);
    on = true;
  }
  if (pData[0] & 0x01) {
    if (on) {
      on = false;
      return;
    }
    digitalWrite(GPIO_NUM_10, HIGH);
  }
}

/** Callback to process the results of the last scan or restart it */
void scanEndedCB(NimBLEScanResults results) { Serial.println("Scan Ended"); }

/** Create a single global instance of the callback class to be used by all
 * clients */
static ClientCallbacks clientCB;

/** Handles the provisioning of clients and connects / interfaces with the
 * server */
bool connectToServer() {
  NimBLEClient *pClient = nullptr;

  /** Check if we have a client we should reuse first **/
  if (NimBLEDevice::getClientListSize()) {
    /** Special case when we already know this device, we send false as the
     *  second argument in connect() to prevent refreshing the service database.
     *  This saves considerable time and power.
     */
    pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
    if (pClient) {
      if (!pClient->connect(advDevice, false)) {
        Serial.println("Reconnect failed");
        return false;
      }
      Serial.println("Reconnected client");
    }
    /** We don't already have a client that knows this device,
     *  we will check for a client that is disconnected that we can use.
     */
    else {
      pClient = NimBLEDevice::getDisconnectedClient();
    }
  }

  /** No client to reuse? Create a new one. */
  if (!pClient) {
    if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
      Serial.println("Max clients reached - no more connections available");
      return false;
    }

    pClient = NimBLEDevice::createClient();

    Serial.println("New client created");

    pClient->setClientCallbacks(&clientCB, false);
    /** Set initial connection parameters: These settings are 15ms interval, 0
     * latency, 120ms timout. These settings are safe for 3 clients to connect
     * reliably, can go faster if you have less connections. Timeout should be a
     * multiple of the interval, minimum is 100ms. Min interval: 12 * 1.25ms =
     * 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout
     */
    pClient->setConnectionParams(12, 12, 0, 51);
    /** Set how long we are willing to wait for the connection to complete
     * (seconds), default is 30. */
    pClient->setConnectTimeout(5);

    if (!pClient->connect(advDevice)) {
      /** Created a client but failed to connect, don't need to keep it as it
       * has no data */
      NimBLEDevice::deleteClient(pClient);
      Serial.println("Failed to connect, deleted client");
      ESP.restart();
      return false;
    }
  }

  if (!pClient->isConnected()) {
    if (!pClient->connect(advDevice)) {
      Serial.println("Failed to connect");
      ESP.restart();

      return false;
    }
  }

  Serial.print("Connected to: ");
  Serial.println(pClient->getPeerAddress().toString().c_str());
  Serial.print("RSSI: ");
  Serial.println(pClient->getRssi());

  /** Now we can read/write/subscribe the charateristics of the services we are
   * interested in */
  NimBLERemoteService *pSvc = nullptr;
  //  NimBLERemoteCharacteristic *pChr = nullptr;
  std::vector<NimBLERemoteCharacteristic *> *pChrs = nullptr;

  NimBLERemoteDescriptor *pDsc = nullptr;

  pSvc = pClient->getService(bCoreUUID);
  if (pSvc) { /** make sure it's not null */
    pChrs = pSvc->getCharacteristics(true);
  }

  if (pChrs) { /** make sure it's not null */

    for (int i = 0; i < pChrs->size(); i++) {
      if (pChrs->at(i)->getUUID().equals(getFunctionUUID)) {
        Serial.println("getFunctionChars found.");
        Serial.printf("data:%02X \n", pChrs->at(i)->readUInt16());
      }
      else if (pChrs->at(i)->getUUID().equals(burstCommandUUID)) {
        Serial.println("burstCommandChars found.");
        burstCommandCharacteristic = pChrs->at(i);
      }
      else if (pChrs->at(i)->getUUID().equals(getBatteryUUID)) {
        Serial.println("getBatteryChars found.");
        getBatteryCharacteristic = pChrs->at(i);
      }
    }
  }

  else {
    Serial.println("DEAD service not found.");
  }

  Serial.println("Done with this device!");
  digitalWrite(GPIO_NUM_10, LOW);

  return true;
}
PortHub porthub;
void setup() {
  Serial.begin(115200);
  Serial.println("Starting NimBLE Client");
  M5.begin();
  Wire.begin(32, 33);
  porthub.begin();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setRotation(1);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);

  pinMode(GPIO_NUM_10, OUTPUT);
  digitalWrite(GPIO_NUM_10, HIGH);

  NimBLEDevice::init("");
  NimBLEDevice::setSecurityAuth(
      /*BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM |*/
      BLE_SM_PAIR_AUTHREQ_SC);

  NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
  NimBLEScan *pScan = NimBLEDevice::getScan();

  pScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks());

  pScan->setInterval(10000);
  pScan->setWindow(9999);

  pScan->setActiveScan(true);
  pScan->start(scanTime, scanEndedCB);
}
byte burstCommandBuffer[7];

uint8_t HUB_ADDR[6] = {HUB1_ADDR, HUB2_ADDR, HUB3_ADDR,
                       HUB4_ADDR, HUB5_ADDR, HUB6_ADDR};

void sendBurstCommand(uint8_t ch0, uint8_t ch1) {
  burstCommandBuffer[0] = ch0;
  burstCommandBuffer[1] = ch1;
  // burstCommandBuffer[2] = burstCommandBuffer[2] & 0x01 ? 0x00 : 0x01;
  burstCommandBuffer[2] = 0x80;
  burstCommandBuffer[3] = 0x80;
  burstCommandBuffer[4] = 0x80;
  burstCommandBuffer[5] = 0x80;
  burstCommandBuffer[6] = 0x80;
  burstCommandCharacteristic->writeValue(burstCommandBuffer);
}

bool motorFlag = false;

void loop() {
  M5.update();
  if (M5.BtnA.wasReleased()) {
    uint16_t voltage = getBatteryCharacteristic->readUInt16();
    Serial.printf("voltage:%d mV\n", voltage);

    motorFlag = !motorFlag;
  }

  if (M5.BtnB.wasReleased()) {
    M5.Axp.PowerOff();
  }

  if (motorFlag) {

    M5.Lcd.setCursor(0, 0);
    M5.Lcd.fillScreen(BLACK);

    // 13-407-703
    // M5.Lcd.printf("%d:%d\n%d:%d", 0, porthub.hub_a_read_value(HUB_ADDR[0]),
    // 1,
    //               porthub.hub_a_read_value(HUB_ADDR[1]));
    uint16_t value0 = porthub.hub_a_read_value(HUB_ADDR[0]);
    uint16_t value1 = porthub.hub_a_read_value(HUB_ADDR[1]);

    if (value0 < 360) {
      value0 = ::map(value0, 360, 10, 200, 255);
    }
    else if (480 < value0) {
      value0 = ::map(value0, 440, 710, 50, 0);
    }
    else {
      value0 = 0x0080;
    }
    if (value0 != 0x0080) {
      porthub.hub_wire_setBrightness(HUB_ADDR[0], 1);
      porthub.hub_wire_fill_color(HUB_ADDR[0], 0, 15, rand() % 250,
                                  rand() % 250, rand() % 250);
    }
    else {
      porthub.hub_wire_setBrightness(HUB_ADDR[0], 0);
      porthub.hub_wire_fill_color(HUB_ADDR[0], 0, 15, 0, 0, 0);
    }

    if (value1 < 360) {
      value1 = ::map(value1, 360, 10, 200, 255);
    }
    else if (480 < value1) {
      value1 = ::map(value1, 440, 710, 50, 0);
    }
    else {
      value1 = 0x0080;
    }
    if (value1 != 0x0080) {
      porthub.hub_wire_setBrightness(HUB_ADDR[1], 1);
      porthub.hub_wire_fill_color(HUB_ADDR[1], 0, 15, rand() % 250,
                                  rand() % 250, rand() % 250);
    }
    else {
      porthub.hub_wire_setBrightness(HUB_ADDR[1], 0);
      porthub.hub_wire_fill_color(HUB_ADDR[1], 0, 15, 0, 0, 0);
    }

    //  M5.Lcd.printf("%d:%d\n%d:%d", 0, ::map(value0, 12, 704, 255, 0), 1,
    //  ::map(value1, 12, 704, 255, 0));

    sendBurstCommand(value0, value1);
    M5.Lcd.printf("0:%04X\n1:%04X", value0, value1);
  }

  if (doConnect) {
    if (connectToServer()) {
      Serial.println("Success! we should now be getting notifications, "
                     "scanning for more!");
    }
    else {
      Serial.println("Failed to connect, starting scan");
    }
    doConnect = false;
  }
  delay(100);
}

```
</div>
</details>