プロミスキャスモードを用いたESP8266でのAmazon Dash Buttonのイベント取得

#概要
プロミスキャスモードのESP8266を用いることで、Amazon Dash Button (ADB)のイベント取得に成功しました。
これによりRaspberry Pi等を必要とせずにESP8266単独での検出が可能となりました。今回はESP-WROOM-02, Arduino 1.6.13を利用しています。

## ESP8266とは
秋月電子等で購入可能なWiFi通信可能なモジュール(500~600円程度)。Arduinoで開発可能。
Wi-Fiモジュール ESP-WROOM-02 DIP化キット　http://akizukidenshi.com/catalog/g/gK-09758/

## Amazon Dash Buttonとは
ボタンを押すだけで、自動注文により商品を届けてくれるIoTデバイス。今回のように注文せずに、ボタン押下イベント取得だけをしたい場合は「ボタンが押されると届く商品の設定」をキャンセルする必要があります。

### 参考
- Amazon Dash Buttonの仕組みとハック: http://qiita.com/dkawashi/items/e6621c4b712b509c73ec
- Amazon Dash ButtonをただのIoTボタンとして使う: http://qiita.com/jsoizo/items/3b8bba4160f41aef20f4

## WiFiチャンネルとボタンのMACアドレスを取得
プロミスキャスモードでの動作にはWiFiチャンネルが、またAmazon Dash Buttonのイベント取得にはMACアドレスが必要となります。

それらを取得するために、まず以下のスケッチをESP8266に書き込み、実行します。

```c:ESP_Promiscuous_getDashInfo.ino
#include <ESP8266WiFi.h>
#include "ESP_Promiscuous.h"

//プロミスキャスモードのAPIを利用するために必要
extern "C" {
  #include <user_interface.h>
}

//WiFi接続部 参考:http://qiita.com/azusa9/items/7f78069cb09872cf6cbf
char toSSID[] = "SSID";
char ssidPASSWD[] = "Password";

//プロミスキャスモードでパケットを受け取ったときのcallback
static void ICACHE_FLASH_ATTR promisc_cb(uint8_t *buf, uint16_t len)
{
  if (len != 128) return; //In order to discard both unknown packets and data packets
  
  struct sniffer_buf2 *sniffer = (struct sniffer_buf2*) buf;
  struct MAC_header *mac = (struct MAC_header*) sniffer->buf;

  int i;
  boolean beaconFlag = true;

  for (i=0; i<6; i++) if (mac->addr2[i] != mac->addr3[i]) beaconFlag = false;

  if (beaconFlag) return; //In order to remove beacon Packet

  Serial.print("Possible MAC Address: ");
  for (i=0; i<6; i++){
    Serial.print(mac->addr2[i], HEX);
    Serial.print(":");
  }
  Serial.println("");
}

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

  WiFi.mode(WIFI_STA);
  WiFi.begin(toSSID, ssidPASSWD);

  Serial.print("WiFi connecting.");

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }

  Serial.println("");

  channel = wifi_get_channel();
  Serial.print("Your WiFi Channel: ");
  Serial.println(channel);
  WiFi.disconnect();

  Serial.println("");
  
  Serial.println("ESP8266 promiscuous mode started.");
  Serial.println("Please push Amazon Dash Button");
  Serial.println("");

  wifi_set_opmode(STATION_MODE);
  wifi_set_channel(channel);
  wifi_set_promiscuous_rx_cb(promisc_cb);

  wifi_promiscuous_enable(1);
}

void loop() { }
```

```c:ESP_Promiscuous.h
struct RxControl {
    signed rssi:8;
    unsigned rate:4;
    unsigned is_group:1;
    unsigned:1;
    unsigned sig_mode:2;
    unsigned legacy_length:12;
    unsigned damatch0:1;
    unsigned damatch1:1;
    unsigned bssidmatch0:1;
    unsigned bssidmatch1:1;
    unsigned MCS:7;
    unsigned CWB:1;
    unsigned HT_length:16;
    unsigned Smoothing:1;
    unsigned Not_Sounding:1;
    unsigned:1;
    unsigned Aggregation:1;
    unsigned STBC:2;
    unsigned FEC_CODING:1;
    unsigned SGI:1;
    unsigned rxend_state:8;
    unsigned ampdu_cnt:8;
    unsigned channel:4;
    unsigned:12;
};
 
struct LenSeq {
    uint16_t length;
    uint16_t seq;
    uint8_t  address3[6];
};

struct sniffer_buf {
    struct RxControl rx_ctrl;
    uint8_t buf[36];
    uint16_t cnt;
    struct LenSeq lenseq[1];
};

struct sniffer_buf2{
    struct RxControl rx_ctrl;
    uint8_t buf[112];
    uint16_t cnt;
    uint16_t len;
};

struct MAC_header {
    uint16_t frameControl;
    uint16_t duration;
    uint8_t addr1[6];
    uint8_t addr2[6];
    uint8_t addr3[6];
    uint16_t sequenceControl;
    uint8_t addr4[6]; 
};
```

そして実行し、ボタンを押すと検出してくれます。
2行目の`5`がWiFiチャネル、`12:34:56:78:9a:bc`がMACアドレスとなります。
Amazon Dash Buttonが関連する通信が10~15個程、表示されます。
ARPプローブ等が送受信されてると思いますが、詳細は把握しておりません。

```
WiFi connecting......
Your WiFi Channel: 5

ESP8266 promiscuous mode started.
Please push Amazon Dash Button.

Possible MAC Address: 12:34:56:78:9a:bc:
Possible MAC Address: 12:34:56:78:9a:bc:
(以下、続く)
```


## イベント取得
続いて、得られたWiFiチャンネルとMACアドレスを用いて、Amazon Dash Buttonの通信を検知します。

**2017/01/02 21:55修正**: ご指摘頂きました方々にお礼申し上げます。また本ソースではESP8266WiFi.hの関数を用いていないので、include無しでもコンパイルが通ると思いますが如何でしょうか。不備等ございましたら、ご指摘いただけると幸いです。

```c:ESP_Promiscuous_DashCapt(ver.0.2).ino
#include "ESP_Promiscuous.h"

extern "C" {
  #include <user_interface.h>
}

byte channel = 5;  //WiFi channel (1-13)
uint8_t targetMAC1[6] = { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc};  //Amazon Dash Button MAC address
int detectInterval = 8000; //msec

unsigned long lastDetectedMillis = 0;
boolean ADBdetectingNow = false;  //Amazon Dash Buttonイベント検出中フラグ

static void ICACHE_FLASH_ATTR promisc_cb(uint8_t *buf, uint16_t len)
{
    if (ADBdetectingNow) return;  //Amazon Dash Buttonイベント検知中(detectInterval指定ミリ秒間)であれば無視

    if (len == 12){
      //No accurate information about MAC address and length of the head of packet.
      struct RxControl *sniffer = (struct RxControl*) buf;
      return;
    } else if (len == 128) {
      //Management Packet
      struct sniffer_buf2 *sniffer = (struct sniffer_buf2*) buf;
      struct MAC_header *mac = (struct MAC_header*) sniffer->buf;

      int i;
      boolean MAC_Matching_Flag = true;
        
      for (i=0; i<6; i++) if (mac->addr2[i] != targetMAC1[i]) MAC_Matching_Flag = false;

      if (!MAC_Matching_Flag) return; //No hit
      
      //Handle it.
      Serial.println("ADB push detected.");
      ADBdetectingNow = true;
      lastDetectedMillis = millis();

      return;

    } else {
      //Data Packet
      struct sniffer_buf *sniffer = (struct sniffer_buf*) buf;
      struct MAC_header *mac = (struct MAC_header*) sniffer->buf;

      return;
    }
}

void setup() {  
  Serial.begin(115200);
  wifi_set_opmode(STATION_MODE);
  wifi_set_channel(channel);
  wifi_set_promiscuous_rx_cb(promisc_cb);

  // Start!
  wifi_promiscuous_enable(1);
}

void loop() {
  unsigned long interval;
  if (lastDetectedMillis > millis()) //稼働時間が49.7日を超えmillis()がオーバーフローした場合
    interval = (0xffffffff - lastDetectedMillis) + millis();
  else
    interval = millis() - lastDetectedMillis;
    
  if (interval > detectInterval)
    ADBdetectingNow = false;
}
```
上記スケッチ起動後にADBのボタンを押すと`ADB push detected.`と表示されるはずです。

#結論
ADBのイベントを取得し、Lチカ等への展開ができそうです。
欠点としてESP8266はプロミスキャスモードでの動作時、WiFiの接続ができなくなってしまう点が挙げられます。
しかしプロミスキャスモードを解除すれば、WiFiの接続ができるようになるので、Milkcocoa等の他のサービスと連携も可能です。

後日、その詳細を書きたいと思います。
12/30 23:38追記 [ESP8266によるAmazon Dash ButtonとMilkcocoaとの連携](http://qiita.com/kat-kai/items/c898a439bafe5e605dae) 書きました

またAPIリファレンスを眺めていると、WPSを利用してWiFi接続できそうだったり、WiFi経由でSleepから起こせそうだったりと色々と面白そうなAPIがありましたので、今後そういったことも記事にしていきたいと思います。



## 参考文献
- WiFi接続
  - http://qiita.com/azusa9/items/7f78069cb09872cf6cbf
- Amazon Dash Button
  - http://qiita.com/jsoizo/items/3b8bba4160f41aef20f4
  - http://qiita.com/dkawashi/items/e6621c4b712b509c73ec