ダイソー BLE リモートシャッター で SwitchBot を操る

(追記)2020/07/25
 本稿で使用しているライブラリはお薦めしません。
 令和バージョンとして記事を書き直しました。
ダイソー BLE リモートシャッター で SwitchBot を操る ~令和バージョン~ - ブログ/こばさんの wakwak 山歩き

 ツイッターとやらを始めるとブログ更新が億劫になってくる・・・それは都市伝説ではなくて本当の話だったのですね。
 「××出来たっ」って報告を上げたらそれで満足しちゃいがちですが、「自慢はいいから、どうやったら出来るのか具体的に教えてくれ」っていうのがインターネッツだと思うので、やっぱ面倒くさいとは言え個人的に有益と思う情報(探し求めている人がいるに違いない情報)は、ちゃんとまとめてアーカイブしておかねばいけないと思う次第。


 公式には1台の BLEデバイス しか接続できない ESP32 の Arduino ライブラリですが、ESP32 としては少なくとも3台の同時接続を標準でサポートしています。
 また、Espressif の気分次第では(SDK側の準備次第では)、もっと多くのデバイスサポートされそうという風なのですが、arduino-esp32 の対応を待っていたのではいつになるか分からないため、不肖ながらも私めが恐れ多くも本家 ESP32_BLE_Arduino から Fork させていただき、複数デバイスへの同時接続に対応 させて頂きました。

 この辺は先にツイッターで呟いてます。
 もっと早くに知りたかったよって人は是非ともフォロー してやってくださいませ

 いちお、アドバタイズで拾い集めた端末情報を一覧にして、Notify に対応してる Characteristic はコールバック登録して Notify の内容をダンプするだけのサンプル(/)も付けておきましたが、いまいち実証的でありません。


 そこで、ダイソーBluetooth リモートシャッターと、普通の機器を無理矢理 IoT 化するサンワサプライ Switch Bot(400-RC005) 、計2台の BLEデバイス を1台の ESP32 でペアリングさせ、リモートシャッターを使って Switch Bot を操作する、ということをやってみたいと思います。


http://dl.ftrans.etr.jp/?eac9464fe8764c31be87d1af847d74b3073a769b.jpg


■ ESP32_BLE_Arduino を差し替える
 すでに Arduino IDEarduino-esp32 は導入済みだと仮定して話を進めます。


 本家のままだと複数デバイスには対応しておらず、また同一の UUID を持つ Characteristic にも対応しておらず、色々と不備が目立つので私めが公開しているものに一部を差し替えます。


 github から 私めのリポジトリ をダウンロードして適当な場所に解凍してください。
 解凍したフォルダごと本家に上被せする形でコピーします。
 Windows の場合だったら、普通は %USERPROFILE%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.0\libraries\BLE が本家の位置なので、ごっそり丸コピーしてやってください。


 丸コピーに抵抗ある方は、以下の変更ファイルのみコピーでも構いません。

  • examples/ble_clients_1/ble_clients_1.ino
  • examples/ble_clients_2/ble_clients_2.ino
  • src/BLEClient.cpp
  • src/BLEClient.h
  • src/BLEDevice.cpp
  • src/BLEDevice.h
  • src/BLERemoteService.cpp
  • src/BLERemoteService.h

 いろいろと改良したとは言え前方互換は維持しているつもりなので、複数デバイスを想定していない旧来のスケッチもそのまま動作するはずです。


■ メインのスケッチ

#include "BLEDevice.h"

static BLEUUID UUID_ABSHUTTER("00001812-0000-1000-8000-00805f9b34fb");
static BLEUUID CHAR_ABSHUTTER("00002a4d-0000-1000-8000-00805f9b34fb");
static BLEUUID UUID_SWITCHBOT("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
static BLEUUID CHAR_SWITCHBOT("cba20002-224d-11e6-9fb8-0002a5d5c51b");

static BLEClient* BLEC_ABSHUTTER = NULL;
static BLEClient* BLEC_SWITCHBOT = NULL;

static uint16_t clicked_button = 0;

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    if (advertisedDevice.haveServiceUUID()) {      
      if(advertisedDevice.getServiceUUID().equals(UUID_ABSHUTTER))  {
        BLEC_ABSHUTTER = BLEDevice::createClient(advertisedDevice.getAddress());
        Serial.print("registered:");
      } else if(advertisedDevice.getServiceUUID().equals(UUID_SWITCHBOT))  {
        BLEC_SWITCHBOT = BLEDevice::createClient(advertisedDevice.getAddress());
        Serial.print("registered:");
      } else
        Serial.print("           ");
        
      Serial.printf("%s %s", advertisedDevice.getServiceUUID().toString().c_str(), advertisedDevice.getName().c_str());
      Serial.println();

      if(BLEC_ABSHUTTER && BLEC_SWITCHBOT)
        advertisedDevice.getScan()->stop();
    }
  }
};

static void notifyCallback(BLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify)
{
  if(length == 2 && !clicked_button)
      clicked_button = pData[0] << 8 | pData[1];
}

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

  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);  
  pBLEScan->start(30);
}

void loop() {
  if(BLEC_ABSHUTTER && BLEC_ABSHUTTER->isDisconnected())
    if(BLEC_ABSHUTTER->connect()) {
      Serial.println("connected:ABShutter");
      auto* pCharacteristicMap = BLEC_ABSHUTTER->getService(UUID_ABSHUTTER)->getCharacteristicsByHandle();
      for(auto itr : *pCharacteristicMap)
        if(itr.second->getUUID().equals(CHAR_ABSHUTTER))
          itr.second->registerForNotify(notifyCallback);
    }

  if(BLEC_SWITCHBOT && BLEC_SWITCHBOT->isDisconnected() && BLEC_ABSHUTTER && BLEC_ABSHUTTER->isConnected())
  {
      BLEC_SWITCHBOT->connect();
      Serial.println("connected:SwitchBot");
  }
  
  if(BLEC_SWITCHBOT && BLEC_SWITCHBOT->isConnected() && clicked_button)
  {
    Serial.printf("button pressed:%04x", clicked_button);
    Serial.println();
    uint8_t buf[] = {0x57, 0x01, 0x00};
    switch(clicked_button)  {
      case 0x0100:  // Volume Up
        buf[2] = 0x03;
        break;
      case 0x0200:  // Volume Down      
        buf[2] = 0x04;
        break;
      case 0x0028:  // Enter
        break;
    }
    BLEC_SWITCHBOT->getService(UUID_SWITCHBOT)->setValue(CHAR_SWITCHBOT, std::string((char *)buf, sizeof(buf)));
    clicked_button = 0;
  }
}

 起動直後の30秒間がペアリングタイムです。
 リモートシャッター・SwitchBot の両方を見つけたら30秒を待たずに直ちにスキャンを終了します。


 なお、SwitchBot は電池をセットして初めてペアリングした相手を親だと記憶し続けるみたいなので、ESP32 とペアリングできないときは電池を抜くか、電池のそばにあるタクトスイッチ(出荷時に戻すボタンぽい)を押すかして過去の記憶は忘れてもらいましょう。


 ペアリング後は3つの操作に対応しています。
 割当てが気に入らない人はテキトーにソースを弄ってください。

リモートシャッター 内部での識別コード SwitchBot 備考
iOSボタン 0x0100 Turn On
Androidボタン 0x0028+0x0100 Press 動作中のため 0x0100 は無視される
(改造版)Androidボタン 0x0200 Turn Off 改造方法は こちら

 しばらく操作しないでいると、どちらも節電のためスリープモードに突入し、ESP32 との接続も切断されます。
 リモートシャッターのほうは OFF/ON するかスイッチ操作しないと復帰しませんが、ちゃんと(リセットかける必要なく)自動再接続されるようになってます。


 起動時にペアリングした相手を記憶していて、それとのみ再接続されるようにしてあるので、他の機器に割り込まれる心配もなく(mac アドレスのみによる無認証であるものの)一定のセキュリティを確保しています。


 接続処理 connect() のところで処理がブロッキングされますが、これは元々のライブラリが RTOSマルチタスク)前提になっているためです。
 本稿のコードではその辺を端折ってますため、ブロッキングされたように見えますが、本来は BLEClient ごとにタスクを分けてセマフォを使ってタスク間通信の処理を書くのがセオリーのようです。


 notifyCallback() と loop() とは別タスクで同時に並行して動いているので、その点も注意が必要です。