ダイソー 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 の内容をダンプするだけのサンプル(1/2)も付けておきましたが、いまいち実証的でありません。
そこで、ダイソーの Bluetooth リモートシャッターと、普通の機器を無理矢理 IoT 化するサンワサプライ Switch Bot(400-RC005) 、計2台の BLEデバイス を1台の ESP32 でペアリングさせ、リモートシャッターを使って Switch Bot を操作する、ということをやってみたいと思います。
■ ESP32_BLE_Arduino を差し替える
すでに Arduino IDE や arduino-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() とは別タスクで同時に並行して動いているので、その点も注意が必要です。
ESP32 と ダイソー Bluetooth リモコンシャッター で Lチカ(無改造版)
ESP32 の Arduinoライブラリ の中の Bluetooth 関係 には、「デバイス内に 同一な UUID を有する複数 Characteristic の存在が考慮されていない」という不具合があって、そのせいで素の(販売されている)状態の ダイソー Bluetooth リモコンシャッター の2つあるボタンを識別できません。
そのため、一つ前の記事 では、リモートシャッター側を改造することで対処しましたが、不具合のあるライブラリ側を改修すればリモートシャッターを改造せずとも2つのボタンを識別できるようになります。
公式ライブラリが修正されるのを待つのがセオリーかと思いますが、待ってられない、って方に向けて自力で何とかする方法について記述いたします。
■不具合のあるライブラリの修正
- libraries/ble/src/BLERemoteService.h
- libraries/ble/src/BLERemoteService.cpp
が修正対象となります。
BLERemoteService.h を読むと
std::map<uint16_t, BLERemoteCharacteristic *> m_characteristicMapByHandle;
void getCharacteristics(std::map<uint16_t, BLERemoteCharacteristic*>* pCharacteristicMap);
という風に、ハンドル番号に紐付いた Characteristic を収納するための map 変数、およびその map 変数を受け取るメソッドが宣言されているにも関わらず BLERemoteService.cpp の中で実装がなされていないという残念な状態。。。
m_characteristicMapByHandle を使うように手を加えるとともに、引数で受け取る getCharacteristics は呼び出し側が面倒になるので、getCharacteristics() と同じ使い勝手になるような getCharacteristicsByHandle() という物を新設しました。
※ getCharacteristicsByHandle() というネーミングは @coppercele さん発案
いつもはここでソースをベタ貼りするところですが、修正前後の差分を見ながら手作業で修正したい方もお見えになると思うので、本家から Fork した私のリポジトリへのリンクを張っておきますね
リポジトリ全体 (src の中の BLERemoteService.cpp と BLERemoteService.h)
差分
いずれ公式ライブラリも改修されるとは思うんですが、なんとなく getCharacteristicsByHandle() が採用されるような気がしてます。
(追記)2018/10/17
デストラクト処理にバグ(delete する必要のないものを delete して2重解放してしまっていた)を 見つけて 修正していますので、10/16 までにソースを持って行かれた方は差し替えて下さい。
上のリンクは最新に繋がってるので、今は大丈夫です。
■メインのスケッチ
#include "BLEDevice.h" static int GPIO_LED_VOLUMEUP = 2; static int GPIO_LED_ENTER = 0; static uint16_t GATT_HID = 0x1812; static BLEUUID GATT_HID_REPORT((unsigned short)0x2a4d); static BLEAddress *pServerAddress = NULL; class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { if (advertisedDevice.haveServiceUUID() && advertisedDevice.getServiceUUID().equals(GATT_HID)) { advertisedDevice.getScan()->stop(); pServerAddress = new BLEAddress(advertisedDevice.getAddress()); Serial.print("found device:"); Serial.println(pServerAddress->toString().c_str()); } } }; static void notifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { uint16_t value = pData[0] << 8 | pData[1]; if(value == 0x0100) digitalWrite(GPIO_LED_VOLUMEUP, !digitalRead(GPIO_LED_VOLUMEUP)); else if(value == 0x0028) digitalWrite(GPIO_LED_ENTER , !digitalRead(GPIO_LED_ENTER )); } void setup() { Serial.begin(115200); pinMode(GPIO_LED_VOLUMEUP, OUTPUT); pinMode(GPIO_LED_ENTER , OUTPUT); BLEDevice::init(""); BLEScan* pBLEScan = BLEDevice::getScan(); pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan->setActiveScan(true); pBLEScan->start(30); } void loop() { static boolean connected = false; if(pServerAddress != NULL && !connected) { BLEClient* pClient = BLEDevice::createClient(); pClient->connect(*pServerAddress); BLERemoteService* pRemoteService = pClient->getService(GATT_HID); if(pRemoteService) { auto* pCharacteristicMap = pRemoteService->getCharacteristicsByHandle(); for (auto itr : *pCharacteristicMap) { Serial.print(itr.second->toString().c_str()); if(GATT_HID_REPORT.equals(itr.second->getUUID()) && itr.second->canNotify()) { itr.second->registerForNotify(notifyCallback); Serial.print(" registered"); } Serial.println(); } for(int i=0; i<2; i++) { digitalWrite(GPIO_LED_VOLUMEUP, HIGH); digitalWrite(GPIO_LED_ENTER , HIGH); delay(100); digitalWrite(GPIO_LED_VOLUMEUP, LOW); digitalWrite(GPIO_LED_ENTER , LOW); delay(400); } connected = true; } } }
コンパイルが通らないときは、一つ前の作業に問題がある(BLERemoteService が修正できていない)可能性が大です。
いつものとおり、LED を繋いだピンは適宜変更してください。(GPIO_LED_VOLUMEUP/GPIO_LED_ENTER)
0x1812 を吹くデバイスを見つけたら節操なく繋ぎに行ってしまう点は、MACアドレスを精査するなり、BLEAdvertisedDevice->getName() して機器名を判別するなりテキトーに応用ください。
Android ボタンを押すと2つの LED が共に変化しますが、これは Android ボタンが Enter と VolumeUp の2つのボタンを押したように振る舞うためです。
きちんと Android ボタンと iOS ボタンとを識別したい場合、Android ボタンの判定は Enter 待ちでいいですが、VolumeUp はどちらもボタンであっても Notify されるので、直前に Enter が押されてない場合に限って iOS ボタン という風なロジックが必要になってきます。
それを踏まえると、VolumeDown に改造したほうが判定がシンプルになって都合がいいですねー
ダイソーの Bluetooth リモコンシャッター は利口なもんで、しばらく使わないでいると勝手にスリープに突入します。
んでボタン押すと始動(恐らく電源ONと同じ)します。
実用的に使おうとする場合、ESP32 側にも切断検知と再接続の処理も必要になります。
BLEClient に isConnected() ってメソッドがあるので、こいつで監視したらいいのかな?
(追記)2018/11/14
Fork したリポジトリをバージョンアップして、複数デバイスへの同時接続と再接続処理を追加しました。
その上で ダイソー BLE リモートシャッター で SwitchBot を操る という本稿の続編になりそうな記事を書きましたので、興味ある方はどうぞ。