ESP32 と ダイソー Bluetooth リモコンシャッター で Lチカ(無改造版)

 ESP32 の Arduinoライブラリ の中の Bluetooth 関係 には、「デバイス内に 同一な UUID を有する複数 Characteristic の存在が考慮されていない」という不具合があって、そのせいで素の(販売されている)状態の ダイソー Bluetooth リモコンシャッター の2つあるボタンを識別できません。
 そのため、一つ前の記事 では、リモートシャッター側を改造することで対処しましたが、不具合のあるライブラリ側を改修すればリモートシャッターを改造せずとも2つのボタンを識別できるようになります。
http://dl.ftrans.etr.jp/?a6641b963c11409a9837db580c3beba32c1336b5.jpg


 公式ライブラリが修正されるのを待つのがセオリーかと思いますが、待ってられない、って方に向けて自力で何とかする方法について記述いたします。


■不具合のあるライブラリの修正

  • 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 までにソースを持って行かれた方は差し替えて下さい。
 上のリンクは最新に繋がってるので、今は大丈夫です。

差分/BLERemoteService::removeCharacteristics()

■メインのスケッチ

#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 を操る という本稿の続編になりそうな記事を書きましたので、興味ある方はどうぞ。