ESP32からBLE接続し、SwitchBotプラグも操作したい

# 概要
以前に投稿した記事ではSwitchBotカーテンをESP32から操作してみました。その続編として今回はSwitchBotプラグをESP32から操作してみます。当初は全く同じようにBLE接続し、操作できると考えていましたが、少し仕様が異なるために修正も必要でした。
![picture.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/5a4095af-bc61-936c-096e-be445104e1cc.jpeg)
# 開発環境
* Windows 11 Home 22H2
* Arduino IDE 2.3.0
* SwitchBot 7.41.5.13
* nRF Connect 4.28.0
# 接続に失敗する問題
以前と同じプログラムを使用しても正しく接続できませんでした。

https://qiita.com/ricelectric/items/38718194342bc90b0fbb
## 原因
SwitchBotカーテンと異なり、SwtichBotプラグのアドバタイズパケットにはサービスUUIDが含まれていないようです。そのため、関数`haveServiceUUID`が偽を返し、接続に失敗していました。
## 対策
単純に該当の部分をコメントアウトすることで接続できるようにはなりましたが、それでも偶に接続できない問題が起きたので調べてみたところ、SwitchBotアプリを起動した状態では接続が失敗していることに気が付きました。よく考えたら接続中は1対1のGATT通信を行うため、当然の結果です。SwitchBotアプリを停止するなど、他の接続を解除してから改めて試してみたところ、今度は確実に接続できました。
# 回路構成
ESP32-DevKitCを使用し、IO22にLED、IO23にスイッチを接続しました。
![breadboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/6abc7d14-9cca-8bf1-6267-7ca368d47b27.png)
# プログラム
SwitchBotプラグに接続できるとLEDが点灯し、切断されるとLEDが消灯します。また、スイッチが押される毎に下記のコマンドを送信し、SwitchBotプラグの状態が切り替わります。コマンドの詳細は[**こちら**](https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/devicetypes/plugmini.md)で公開されています。
| インデックス | 値 | 説明 |
|:-:|:-:|:--|
| 0 | 0x57 | 固定値 |
| 1 | 0x0F | 拡張コマンドを使用 |
| 2 | 0x50 | 固定値 |
| 3 | 0x01 | 固定値 |
| 4 | 0x02 | SwitchBotプラグの状態を切り替え |
| 5 | 0x80 | 同上 |

上記のコマンドを送信すると得られる応答パケットの例を以下に示します。コマンドで操作する直前のSwitchBotプラグの状態が取得できます。
| インデックス | 値 | 説明 |
|:-:|:-:|:--|
| 0 | 0x01 | ステータスコード |
| 1 | 0x00 | SwitchBotプラグの状態を取得 |

実際のスケッチは以下の通りです。
```cpp:switchbot_plug.ino
#include <BLEDevice.h>

#define LED_PIN 22
#define BUTTON_PIN 23

#define MAC_ADDRESS "00:00:00:00:00:00" //put the MAC address of the device

#define SERVICE_UUID "cba20d00-224d-11e6-9fb8-0002a5d5c51b" //service UUID 
#define CHARACTERISTIC_UUID_RX "cba20002-224d-11e6-9fb8-0002a5d5c51b" //characteristic UUID of the message from the terminal to the device
#define CHARACTERISTIC_UUID_TX "cba20003-224d-11e6-9fb8-0002a5d5c51b" //characteristic UUID of the message from the device to the terminal


bool doScan = false;
bool doConnect = false;
bool connectionState = false;

int buttonState = LOW;
int lastButtonState = LOW;

static BLERemoteCharacteristic* pRemoteCharacteristicRX;
static BLERemoteCharacteristic* pRemoteCharacteristicTX;
static BLEAdvertisedDevice* pDevice;


static void notifyCallback(BLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
  uint8_t response = pData[0];
  if (response == 0x01) { //OK
    Serial.println("Executed the command successfully");
  } else {
    Serial.println("Some error occurred while executing the command");
  }
}

class clientCallbacks: public BLEClientCallbacks {
  void onConnect(BLEClient* pClient) {
  }

  void onDisconnect(BLEClient* pClient) {
    connectionState = false;
    digitalWrite(LED_PIN, LOW);
    Serial.println("Disconnected from the device");
  }
};

bool connect() {
  Serial.print("Connecting to ");
  Serial.println(pDevice->getAddress().toString().c_str());
  
  BLEClient* pClient = BLEDevice::createClient();
  Serial.println(" - Created the client");

  //connect to the server
  pClient->setClientCallbacks(new clientCallbacks());
  pClient->connect(pDevice);
  if (!pClient->isConnected()) {
    Serial.println("Failed to connect to the server");
    return false;
  }
  Serial.println(" - Connected to the server");

  //obtain the reference to the service in the server
  BLERemoteService* pRemoteService = pClient->getService(BLEUUID(SERVICE_UUID));
  if (pRemoteService == nullptr) {
    Serial.println("Failed to find the service");
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found the service");

  //obtain the reference to the characteristic RX in the service of the server
  pRemoteCharacteristicRX = pRemoteService->getCharacteristic(BLEUUID(CHARACTERISTIC_UUID_RX));
  if (pRemoteCharacteristicRX == nullptr) {
    Serial.println("Failed to find the characteristic RX");
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found the characteristic RX");

  //obtain the reference to the characteristic TX in the service of the server
  pRemoteCharacteristicTX = pRemoteService->getCharacteristic(BLEUUID(CHARACTERISTIC_UUID_TX));
  if (pRemoteCharacteristicTX == nullptr) {
    Serial.println("Failed to find the characteristic TX");
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found the characteristic TX");

  //register the callback function to receive the notifications from the server
  if (pRemoteCharacteristicTX->canNotify()) {
    pRemoteCharacteristicTX->registerForNotify(notifyCallback); 
  }
  return true;
}

class advertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    BLEAddress address = advertisedDevice.getAddress();
    Serial.println(address.toString().c_str());

    if (address.equals(BLEAddress(MAC_ADDRESS))) {
      BLEDevice::getScan()->stop();
      pDevice = new BLEAdvertisedDevice(advertisedDevice);
      Serial.println("Found the target device");

      doScan = true;
      doConnect = true;
    }
  }
};

void sendCommand() {
  uint8_t command[6];

  command[0] = 0x57; //fixed value
  command[1] = 0x0F; //header
  command[2] = 0x50; //fixed value
  command[3] = 0x01; //fixed value
  command[4] = 0x02; //toggle
  command[5] = 0x80; //toggle
  
  pRemoteCharacteristicRX->writeValue(command, sizeof(command), false); //send the command
}

void setup() {
  Serial.begin(115200);
  Serial.println("Initializing...");
  BLEDevice::init("");

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  
  BLEScan* pScan = BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(new advertisedDeviceCallbacks());
  pScan->setActiveScan(true); //enable active scanning
  pScan->start(5, false); //start the scan to run for 5 seconds
}

void loop() {
  if (doConnect == true) {
    if (connect()) {
      connectionState = true;
      digitalWrite(LED_PIN, HIGH);
      Serial.println("Connected to the device successfully");
    } else {
      Serial.println("Some error occurred while connecting to the device");
    }
    doConnect = false;
  }

  buttonState = digitalRead(BUTTON_PIN);
  if (lastButtonState == HIGH && buttonState == LOW) {
    if (connectionState) {
      sendCommand(); //toggle the state of the plug
    } else if (doScan) {
      BLEDevice::getScan()->start(5, false); //start the scan to run for 5 seconds
    }
  }

  lastButtonState = buttonState;
  delay(100);
}
```
# 動作確認
正常な場合はシリアルモニタに以下のようなメッセージが出力されるはずです。まず、スキャンを開始すると周辺のBLEデバイスを検索し、そのMACアドレスを羅列します。予め指定したMACアドレスと合致していたら接続を試みます。それぞれサービスUUID、キャラクタリスティクスUUIDも正しく取得できたら接続は完了です。その後はスイッチが押される毎にコマンドを送信します。もし、途中で何らかの問題が生じた場合は接続を中断し、改めてスイッチが押されると再接続を試みます。接続が切断された場合も同様です。
```
09:23:10.560 -> 39:18:20:AD:9D:22
09:23:10.560 -> 94:48:89:D6:9C:E0
09:23:10.560 -> 00:00:00:00:00:00
09:23:10.560 -> Found the target device
09:23:10.684 -> Connecting to 00:00:00:00:00:00
09:23:10.684 ->  - Created the client
09:23:10.810 ->  - Connected to the server
09:23:10.949 ->  - Found the service
09:23:11.270 ->  - Found the characteristic RX
09:23:11.544 ->  - Found the characteristic TX
09:23:11.779 -> Connected to the device successfully
09:23:14.100 -> Executed the command successfully
09:23:15.321 -> Executed the command successfully
09:23:23.525 -> Executed the command successfully
```
# まとめ
ここまで容易に外部から操作できてしまうことには少し驚きましたが、普通に実用性も高いため、購入して良かったです。