IOTボタン対決 Amazon Dash ボタン VS ダイソーBLE300円ボタン


IOTボタンとして入手性の高いAmazon Dash ボタンとダイソーBLE300円ボタン
の反応速度を比べてみました。
ESP32を利用します。

# 様子
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">IOTボタン対決やってみました。<br>Amazon Dash ボタン VS ダイソーBLE300円ボタン<br>ファイッ！！<br><br>書いた記事とソースはこちら<a href="https://t.co/IywyZ34ZKj">https://t.co/IywyZ34ZKj</a><a href="https://twitter.com/hashtag/ESP32?src=hash&amp;ref_src=twsrc%5Etfw">#ESP32</a> <a href="https://t.co/TATEyhEPIf">pic.twitter.com/TATEyhEPIf</a></p>&mdash; ode (@odetarou) <a href="https://twitter.com/odetarou/status/1176132037700243456?ref_src=twsrc%5Etfw">September 23, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 結果
Sleep状態からの対決で結果としては互角でした。引き分け。どちらも1秒以内。

２回目の押下時はダイソーBLE側は接続が続いているので瞬間で、Amazon Dash側は連続しては押せない＆少し反応が悪くなるようです。
よくあるケースとしてはたまに１回押すぐらいかと思うのでどちらでもよさそうです。
連続して押すようなケースがある場合はダイソーBLEが良さそうです。
どちらもセキュリティは弱いですが家庭内で重要でない操作をするには充分そうです。(Macアドレスのチェックのみなので偽装されると弱いですが、近所の電波の範囲内でボタンを押したタイミングでMacアドレスを盗む必要あり）

セキュリティー高めで高速性を追求する場合はESP NOWで自作ボタン作るなどが良いかもしれません。

# それぞれの特徴
## Amazon Dash ボタン
- メルカリでまとめ売りで1つあたり100円〜200円ほど。
- 2019年８月末でサービス終了してしまいました。ただしIOTボタンとして引き続き使えます。(初期化しないでも使えるかは不明。利用済みの中古が確実かもしれません)
- Wifiを利用。速度を早めるためにWifiのプローブ要求を検出(セキュリティーは低め）
- 電池：アルカリ単4形電池 約1000回ボタンを押せる(500回や2000回という記事もあり不明) https://halfrack.hatenadiary.org/entry/20161206/1480999343
- 重量：フックなしで25g、フックありで29g

### ESP32対応
以前にESP8266では下記記事を参考にさせて頂きました。ESP32でもできないか確認したところ対応できました。
参考：Amazon Dash ButtonイベントをWIFI_AP_STAモードのESP8266で取得する - Qiita
https://qiita.com/kat-kai/items/182de6857d5dc89a2cf3

ポイントとしては
`WiFi.onEvent`
にてWifiイベントのコールバックをもらえるのでこちらを利用します。
どのようなイベントがあるかは下記サンプルが参考になります。
https://github.com/espressif/arduino-esp32/blob/a3ed5118847614388d6188225174e859682c0a57/libraries/WiFi/examples/WiFiClientEvents/WiFiClientEvents.ino

但しデフォルトの状態だと`WIFI_EVENT_MASK_AP_PROBEREQRECVED`はイベントが来すぎるのでマスクされてOFFにされています。
https://github.com/espressif/esp-idf/blob/aa087667dffb8f024a5bf162cef4d5c0ffb152ab/components/esp_wifi/include/esp_wifi.h#L913

そのため下記にてマスクを解除する必要がありました。
`esp_wifi_set_event_mask(0);`

イベント名と値の一覧は下記headerが参考になります。
https://github.com/espressif/arduino-esp32/blob/d5e2bb12ca02ae9066e9dad84d9dbf268aca6fa3/tools/sdk/include/esp32/esp_event_legacy.h

## ダイソーBLE300円ボタン
- 100均で買える。さすがに100円ではなく300円。
- ボタン操作後90秒後に自動Sleep。BLE接続中の反応速度は瞬間。
- Sleep中からボタンを押すと接続イベントのみでボタンイベントは飛んでこない。そのため今回は接続イベント自体をトリガーにしました。
- パッケージにBluetooth version3.0と書かれていたのですがBLE接続できました。
- 電池：CR2032 ボタン電池 約6ヶ月(1日に10回使用した場合) ※説明書より引用
- 重量：8g
- プラスチック感ありの安物感があるので物としてはDashボタンのほうが良いです。

# コード
``` main.cpp
// Conf系

// Daiso300円BLEボタンのBLE address mac値。
const std::string address = "11:11:11:11:11:11";

// ADB(Amazon Dash Button)のmac値
uint8_t monitoringMAC1[6] = { 0x12, 0x34, 056, 0x78, 0x9a, 0xbc};
uint8_t monitoringMAC2[6] = { 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};






// BLE関連
#include "BLEDevice.h"

//16bitのUUIDを36文字の形式で設定すると動かなかったため、16bit値で指定。
//static BLEUUID SERVICE_UUID_ABSHUTTER("00001812-0000-1000-8000-00805f9b34fb");
static BLEUUID SERVICE_UUID_ABSHUTTER((uint16_t) 0x1812);
//static BLEUUID CHAR_UUID_ABSHUTTER("00002a4d-0000-1000-8000-00805f9b34fb");
static BLEUUID CHAR_UUID_ABSHUTTER((uint16_t) 0x2a4d);

BLEClient*  pClient;
BLEAdvertisedDevice* myDevice = NULL;
BLEAddress* bleAddress = NULL;


// ADB(Amazon Dash Button)関連
#include <WiFi.h>
#include <esp_wifi.h>

boolean detectingMAC1 = false;
boolean detectingMAC2 = false;

// サーボ関連
//use first channel of 16 channels (started from zero)
#define LEDC_CHANNEL_0 0
#define LEDC_CHANNEL_1 1
//use 10 bit precission for LEDC timer
#define LEDC_TIMER_BIT 10
//use 50 Hz as a LEDC base frequency
#define LEDC_BASE_FREQ 50
//ServoPWM pin
#define SRV_PIN1 25
#define SRV_PIN2 27


TaskHandle_t th1;
TaskHandle_t th2;

TaskHandle_t thBle;
TaskHandle_t thAdb;


// BLE関連

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      if (advertisedDevice.haveServiceUUID()) {
        if (advertisedDevice.getServiceUUID().toString() == SERVICE_UUID_ABSHUTTER.toString())  {
          if (address != advertisedDevice.getAddress().toString()) {
            Serial.println("mac address different");
            return;
          }
          myDevice = new BLEAdvertisedDevice(advertisedDevice);          
          bleAddress = new BLEAddress(advertisedDevice.getAddress());
          Serial.print("registered. ");
        } else {
          Serial.print("unknown device. ");
        }
        Serial.println(advertisedDevice.getServiceUUID().toString().c_str());
        Serial.println(SERVICE_UUID_ABSHUTTER.toString().c_str());

        Serial.println(advertisedDevice.toString().c_str());

        if (myDevice != NULL) {
          advertisedDevice.getScan()->stop();
        }
      }
    }
};

// https://stackoverflow.com/questions/3381614/c-convert-string-to-hexadecimal-and-vice-versa
std::string vector_to_hex(const std::vector<byte>& input)
{
  static const char* const lut = "0123456789ABCDEF";
  size_t len = input.size();

  std::string output;
  output.reserve(2 * len);
  for (size_t i = 0; i < len; ++i)
  {
    const unsigned char c = input[i];
    output.push_back(lut[c >> 4]);
    output.push_back(lut[c & 15]);
  }
  return output;
}

class MyClientCallback : public BLEClientCallbacks {
    void onConnect(BLEClient* pclient) {
      Serial.println("onConnect");
      xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 5, &th1, 0); //マルチタスク core 0 実行
    }
    void onDisconnect(BLEClient* pclient) {
      Serial.println("onDisconnect");
    }
};

static void notifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify)
{
  std::vector<byte> data(pData, pData + length);
  std::string dataString = vector_to_hex(data);

  Serial.print("data: ");
  Serial.println(dataString.c_str());

  Serial.println("notify");

  uint16_t clicked_button;
  clicked_button = pData[0] << 8 | pData[1];
  if (clicked_button == 0x0100) {
    xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 5, &th1, 0); //マルチタスク core 0 実行
  }
}


// ADB(Amazon Dash Button)関連

String getStrMAC(uint8_t mac[6]) {
  String res = String(mac[0], HEX) + ":" + String(mac[1], HEX) + ":" + String(mac[2], HEX) + ":" +
               String(mac[3], HEX) + ":" + String(mac[4], HEX) + ":" + String(mac[5], HEX);
  return res;
}

void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
{
  if (event != SYSTEM_EVENT_AP_PROBEREQRECVED) return; //プローブ要求以外は無視

  uint8_t* mac = info.ap_probereqrecved.mac;

  Serial.println("catch : " + getStrMAC(mac));

  if (memcmp(mac, monitoringMAC1, 6) == 0) {
    detectingMAC1 = true;
  }
  if (memcmp(mac, monitoringMAC2, 6) == 0) {
    detectingMAC2 = true;
  }
}

void detectMAC1() {
  Serial.println("detectMAC1");
  xTaskCreatePinnedToCore(Task2, "Task2", 4096, NULL, 5, &th2, 0); //マルチタスク core 0 実行
}

void detectMAC2() {
  Serial.println("detectMAC2");
}

void setup() {
  Serial.begin(115200);

  Serial.println("boot");

  BLEDevice::init("");

  pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallback());

// 直接接続はできなそうなのでコメントアウト。Scanしての流れが必須になっている??
//  if (address == "") {
//  } else {
//    bleAddress = new BLEAddress(address);
//  }

  WiFi.onEvent(WiFiEvent);
  WiFi.softAP("adb_dum", "adb_dum"); // dummy ssidを設定
  esp_wifi_set_event_mask(0); // softAP呼ぶ前に呼ぶと効かなかったので、後で呼ぶようにしている

  xTaskCreatePinnedToCore(loopAdb, "loopAdb", 4096, NULL, 5, &thAdb, 0); //マルチタスク core 0 実行
  xTaskCreatePinnedToCore(loopBle, "loopBle", 4096, NULL, 5, &thBle, 0); //マルチタスク core 0 実行
  Serial.println("setup end");
}

void loop() {

  //   // serial入力によっての処理。
  //   String str = Serial.readStringUntil('\n');
  //   str.trim();
  //   if (str != "") {
  //     Serial.println("serial input: " + str);
  //   }
  //   if (str == "disconnect") {
  //     pClient->disconnect();
  //   }

  delay(200);
}

void loopAdb(void *pvParameters) {
  while (true) {
    static bool isDetected1 = true;
    static bool isDetected2 = true;
    static unsigned long detected1OldTime = 0;
    static unsigned long detected2OldTime = 0;
    unsigned long time = millis();
    
    if (isDetected1 && detectingMAC1) {
      detectMAC1();
      detected1OldTime = time;
      isDetected1 = false;
    }
    // 連続認識防止のため認識後1秒間は無視する
    if (!isDetected1 && (time - detected1OldTime) > 1000 * 1) {
      isDetected1 = true;
      detectingMAC1 = false;
    }
    
    if (isDetected2 && detectingMAC2) {
      detectMAC2();
      detected2OldTime = time;
      isDetected2 = false;
    }
    // 連続認識防止のため認識後1秒間は無視する
    if (!isDetected2 && (time - detected2OldTime) > 1000 * 1) {
      isDetected2 = true;
      detectingMAC2 = false;
    }

    delay(50);
  }
}


void loopBle(void *pvParameters) {
  while (true) {
    if (bleAddress == NULL) {
      BLEScan* pBLEScan = BLEDevice::getScan();
      pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
      pBLEScan->setActiveScan(true);
      pBLEScan->start(30);
    } else if (!pClient->isConnected())
    {
      if (pClient->connect(*bleAddress, myDevice->getAddressType())) {
        Serial.println("connected:ABShutter");

        BLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID_ABSHUTTER);
        if (pRemoteService)
        {
          BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(CHAR_UUID_ABSHUTTER);
          pRemoteCharacteristic->registerForNotify(notifyCallback);

          Serial.print("connected to:");
          Serial.println(pRemoteCharacteristic->toString().c_str());
        }
      } else {
        Serial.println("connect failure.");
      }
    }

    delay(50);
  }
}

void Task1(void *pvParameters) {
  playServo1();
  vTaskDelete(NULL);
}
void Task2(void *pvParameters) {
  playServo2();
  vTaskDelete(NULL);
}

void playServo1() {

  // Setup timer and attach timer to a led pin
  ledcSetup(LEDC_CHANNEL_0, LEDC_BASE_FREQ, LEDC_TIMER_BIT);
  ledcAttachPin(SRV_PIN1, LEDC_CHANNEL_0);

  //サーボモーターを動かす
  for (int i = 0; i < 3; i++) {
    for (int angle = 20; angle <= 85; angle++) {
      // サーボモーターを回転
      ledcWrite(0, angle);
      delay(10);
    }
    for (int angle = 85; angle >= 20; angle--) {
      // サーボモーターを回転
      ledcWrite(0, angle);
      delay(10);
    }
  }
  ledcDetachPin(SRV_PIN1);
}


void playServo2() {

  // Setup timer and attach timer to a led pin
  ledcSetup(LEDC_CHANNEL_1, LEDC_BASE_FREQ, LEDC_TIMER_BIT);
  ledcAttachPin(SRV_PIN2, LEDC_CHANNEL_1);

  //サーボモーターを動かす
  for (int i = 0; i < 3; i++) {
    for (int angle = 20; angle <= 85; angle++) {
      // サーボモーターを回転
      ledcWrite(1, angle);
      delay(10);
    }
    for (int angle = 85; angle >= 20; angle--) {
      // サーボモーターを回転
      ledcWrite(1, angle);
      delay(10);
    }
  }
  ledcDetachPin(SRV_PIN2);
}
```

# コードでの雑記

BLEの扱いはまだ２回めの素人ですが、気になった所としては
ダイソーボタンへのBLE接続としてMACアドレス指定での直接接続はうまくできなかったため、一度ScanしてMACアドレス値を取得するようにしました（これが普通と思われますが）。
以前につないだBLE機器のスマートロックのSesameの場合は直接Macアドレス指定で接続できScan処理は不要だったのですが、機器によって違いがあるのかもしれません。とはいえSesameと違いScanしても充分早いので問題なさそうですね。
Advertisement Intervalは短いと思われます、接続良好そうです。

今回はBLEのセキュリティーはMac値のチェックのみですが、より安全な方法がないかは気になるところです。
Bluetooth3.0での接続なら事前ペアリングでセキュアなのかもですがあまり良くわかっていないです。