スマホを介さず、SwitchBot電球が操作できるリモコンをESP32で作る

# 概要
スマホを介さずにSwitchBotデバイスが操作できるSwitchBotリモコン。確かに便利そうですが、操作できる対象がSwitchBotとSwitchBotカーテンだけなのは少し残念でした。

https://www.itmedia.co.jp/pcuser/articles/2102/18/news067.html

そこで、それ以外のSwitchBotデバイスも操作できるようなリモコンを自作してみました。実はAPIが公開されているので外部から操作するのは意外と簡単です。BLE接続する必要があるため、今回はESP32を使用して作ります。
![picture.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951963/265d66f5-0f76-18d6-a46e-52a4b80896d8.jpeg)
# 開発環境
* Windows 11 Home 22H2
* Arduino IDE 2.3.0
* SwitchBot 7.41.5.13
# 回路構成
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)
# プログラム
[**こちら**](https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/devicetypes/colorbulb.md)で公開されているAPIを利用します。使い方に関しては以前に投稿した記事が参考になると思うので今回は割愛します。

https://qiita.com/ricelectric/items/62ebe63350e3e2df0421

SwitchBot電球に接続できたらLEDが点灯し、切断されたらLEDが消灯します。また、スイッチが押される毎に所定のコマンドを送信し、SwitchBot電球の状態を切り替えます。
```cpp:switchbot_bulb.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[5];

  command[0] = 0x57; //fixed value
  command[1] = 0x0F; //header
  command[2] = 0x47; //fixed value
  command[3] = 0x01; //fixed value
  command[4] = 0x03; //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 bulb
    } else if (doScan) {
      BLEDevice::getScan()->start(5, false); //start the scan to run for 5 seconds
    }
  }

  lastButtonState = buttonState;
  delay(100);
}
```
# まとめ
このように各種SwitchBotデバイスは外部から簡単に操作できます。スイッチの数などを拡張し、オリジナルのリモコンも作ることができそうです。