Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
17
Help us understand the problem. What are the problem?

posted at

updated at

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

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

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

NimBLEのインストール

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

image.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を定義します


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

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


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

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

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


  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


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

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

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が来たら消灯するようにしてみます


// 点灯したあと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);
  }

}

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

image.png

全体のソース

NimbleTest.ino
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);
}


複数デバイスに対応する

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

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

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());
            }

        }
    }
    ;
};


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デバイスが複数存在しても接続することができます

NimbleHidMulti.ino
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);
}


Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
17
Help us understand the problem. What are the problem?