ESP32 と ダイソー Bluetooth リモコンシャッター で Lチカ VolumeDown 版

 1年近く前、ダイソー Bluetooth リモコンシャッター を ESP32 に BLE 接続させることに成功し、ボタンが押されたことを検知するまでは出来たものの(当時の記事)、リモコンシャッターにある2つのボタンを識別すること叶わず、ずっとお蔵入りしていたのですが、1年を経てようやく解決の目を見ることになりました♪
http://dl.ftrans.etr.jp/?8257d5fffed54ccaad0c72f456f08b7bd1c853d6.jpg


 ことの顛末を説明しきるのは大変なので端的に結論を申しますと @coppercele さんがライブラリの不備を見つけて下さった おかげで、ボタンを識別できない原因を突き止めることが出来たのです!
 私は横から糞リプ入れていただけの身ですけど・・・(汗


 公開されているライブラリの不備とは「デバイス内に 同一な UUID を有する複数 Characteristic の存在が考慮されていない」というもの。
 当時、GATT を学習し始めて3日ほどしか経ってない私でしたから、同一 UUID が重複して存在するなんてことを予測できるはずもなく、ライブラリの不具合なんて疑いもしませんでした。


 しかし、ダイソーBluetooth リモコンシャッター には「00002a4d-0000-1000-8000-00805f9b34fb」という UUID な Characteristic が2つあったのです。


 その2つ両方の Characteristic からの Notify を拾っていればボタンを識別できていたのですが、公開ライブラリは最初に見つけた Characteristic だけを有効とし、次に見つけた Characteristic を読み飛ばす(ライブラリの利用者から見ると Characteristic が存在しないように見える)という動きをしていました。


 最初に見つかる Characteristic は Volume キーを始めとするマルチメディアキーと称されるものを通知するためのようで、次に見つかる(ライブラリが読み飛ばす)Characteristic は、通常のキーを通知するために実装されているみたいでした。


 ダイソーBluetooth リモコンシャッター は HIDキーボードとして振る舞います。
 iOS ボタンは VolumeUp を、Android ボタンは Enter + VolumeUp をそれぞれ送出するのですが、VolumeUp ボタンはマルチメディアキーに属するので最初に見つかる Characteristic を通じて通知されるのですが、Enter ボタンは通常のキーの扱いのため、最初のとは異なる Characteristic を通じて通知されるにも関わらず、後者はライブラリが読み飛ばしていたため通知を受けれず、つまり Android ボタンの Enter が届かないでいて VolumeUp だけだもんで iOS ボタンと識別できず・・・そんな風な展開だったようなのです。


 公開されているライブラリのソースを弄ったうえで、Enter + VolumeUp を取得することにも成功しましたが、恐らくは近々にライブラリ側が修正されると思われ、それを待ってもソース公開は遅くなかろう・・・ということで、今回の記事では別の方法で(現時点で公開されている不具合を抱えたままのライブラリで)動く方法をご紹介したいと思います。


 公開ライブラリに手を加えずにボタンを識別する方法とは・・・


 Android ボタンの Enter + VolumeUp を VolumeDown にしてしまう


 公開ライブラリが原因で Enterキー を読めないのだから、Enter を使わなければ(VolumeUp と同じグループに属する VolumeDown にしてしまえば)うまくいくだろ、って話です。


 VolumeDown に変更する方法については こちらの方の記事 の後半部分をご覧下さい。


http://dl.ftrans.etr.jp/?89ac3298b620450da0eb59f75a7fc9e0fb4ac85d.jpg


 改造がうまくいったかどうかは、手持ちスマホとペアリングさせて、音楽でも流しながらボタン操作してみて、音量が上下するかで確認できます。


 そのうえで、VolumeUp と VolumeDown とを識別できるコードを。

#include "BLEDevice.h"

static int GPIO_LED_1 = 2;  // Volume Up 時の LED
static int GPIO_LED_2 = 0;  // Volume Down 時の LED
static uint16_t GATT_HID = 0x1812;
static BLEUUID GATT_HID_REPORT((uint16_t)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)
{
  int LED = -1;
  switch(pData[0])
  {
    case 0x01:  // Volume Up
      LED = GPIO_LED_1;
      break;
    case 0x02:  // Volume Down
      LED = GPIO_LED_2;
      break;
  }
  if(LED >= 0)
    digitalWrite(LED, !digitalRead(LED));
}

void setup() {
  Serial.begin(115200);
  pinMode(GPIO_LED_1, OUTPUT);
  pinMode(GPIO_LED_2, 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)
    {
      BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(GATT_HID_REPORT);
      pRemoteCharacteristic->registerForNotify(notifyCallback);

      Serial.print("connected to:");
      Serial.println(pRemoteCharacteristic->toString().c_str());

      for(int i=0; i<2; i++)
      {
        digitalWrite(GPIO_LED_1, HIGH);
        digitalWrite(GPIO_LED_2, HIGH);
        delay(100);
        digitalWrite(GPIO_LED_1, LOW);
        digitalWrite(GPIO_LED_2, LOW);
        delay(400);
      }

      connected = true;
    }
  }
}

 前回のソース と比較して大きくは変わってません。
 BLEDevice.h の中の人が難しいことを一手に引き受けてくれるので、十分に短いコードで実現できました。


 ボタン操作で LED がモーメンタリーに点滅するのも一緒ですが、ボタン毎に LED を分けてあります。
 iOS ボタンが GPIO_LED_1、Android ボタンが GPIO_LED_2 になってます。



 notifyCallback の中で、0x01 と 0x02 と決め打ちして識別してますが、この辺はダイソーBluetooth リモコンシャッターに依存している部分で、汎用的に作り込むのであれば先にキーマップを読み込んで 0x01、0x02 に対応するキーコードを取得したほうがベターです。


 0x1812/0x2a4b(0x2a4d ではない) を readValue() するとキーマップが得られるので、そこを参照して実際のキーを特定したほうがいいです。
 自分も目下勉強中なので、これ以上の説明はできませんが、たぶん この辺 を読んだら理解できるはず・・・


(予告)
 本記事は ダイソーBluetooth リモコンシャッター の Android ボタンを初期状態の Enter+VolumeUp から VolumeDown に改造したうえでの識別でしたが、未改造状態でボタンを識別するソースコード例も数日内にお披露目しようと思ってます。
 しばしお待ち下さいませ〜


(追記)2018/10/11
 続きを書きました