Amazon Dash ButtonイベントをWIFI_AP_STAモードのESP8266で取得する

##はじめに
今回の記事では、Arduino IDEで開発可能で安価なWiFiモジュールESP8266を用いてAmazon Dashu Buttonのボタン押下イベントを取得しています。これまでの[プロミスキャスモードによる手法](http://qiita.com/kat-kai/items/3b1d5c74138d77a27c4d)ではなく、WIFI_AP_STAモードで取得することで外部サービスとの通信をサクッとすることができます。

Amazon Dash Buttonの通信は以下の通りになっています　([rukihenaさんの記事](http://qiita.com/rukihena/items/aed9999a6a4376d68f0b)より)
>Amazon Dash Button は AmazonのサーバとHTTPSでしゃべるので、それに先立ち以下のような通信をしていると思われます。
>①無線ネゴ→②WPA認証→③DHCP(ARP含む)→④DNS→⑤HTTPS

今回も①無線ネゴの段階で検出しています。①の段階でのAmazon Dash Buttonからのプローブ要求を検知しております。


##実装
(**15:45追記** 注意点: 1度ボタンを押すと2回程度の重複検知が生じますのでご注意ください。)
(**20:28追記** スマホ等の他の機器からのWiFi接続が少し不安定になります。同じSSIDが2つあるため、WiFiの繋ぎ変えが生じていそうです。)

```c:esp8266ADB-AP_STA.ino
#include <ESP8266WiFi.h>

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

//-------------------------------------------------------------------------
//  WiFi Data
//-------------------------------------------------------------------------
#define WLAN_SSID       "yourSSID"
#define WLAN_PASS       "yourPassword"
//-------------------------------------------------------------------------

//String monitoringMAC1 = "12:34:56:78:9a:bc"; //各アドレスにおいて0F以下であれば、0は省略されます。
//String monitoringMAC2 = "a:b:c:d:e:f"; //例えば0a:0b:0c:0d:0e:0fだと"a:b:c:d:e:f"と表記する必要があります。
uint8_t monitoringMAC1[6] = { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc};  //MACアドレスの比較法変更 (2017/01/11 9:15)
uint8_t monitoringMAC2[6] = { 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};

boolean detectingMAC1 = false;
boolean detectingMAC2 = false;

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 ICACHE_FLASH_ATTR wifi_handle_event_cb(System_Event_t *evt) {
  if (evt->event != EVENT_SOFTAPMODE_PROBEREQRECVED) return; //プローブ要求以外は無視

  uint8_t* mac = evt->event_info.ap_probereqrecved.mac;

  if (memcmp(mac,monitoringMAC1,6) == 0) detectingMAC1 = true;  //Thanks to @rukihena san
  if (memcmp(mac,monitoringMAC2,6) == 0) detectingMAC2 = true;
}

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

  connectWiFi();

  //既設アクセスポイントと同一のSSIDでsoftAP作成
  WiFi.softAP(WLAN_SSID, "pleasechangethisdummypassword");
  wifi_set_event_handler_cb(wifi_handle_event_cb);
}

void loop() {
  //milkcocoa->loop();  milkcocoa用
  if (detectingMAC1) {
    detectingMAC1 = false;
    detectMAC1();
  }

  if (detectingMAC2) {
    detectingMAC2 = false;
    detectMAC2();
  }
}

void connectWiFi() {
  WiFi.disconnect();
  delay(10);
  
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(WLAN_SSID, WLAN_PASS);

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

void detectMAC1() {
  Serial.println("ADB1 detected!");
}
void detectMAC2() {
  Serial.println("ADB2 detected!");
}

```
今回はWIFI_AP_STAモード(自身はアクセスポイントになりつつ、既設のアクセスポイントにクライアントとしても接続するモード)を用いています。また複数台検知も容易になりました。

まず`setup()`で無線ルータに接続し、`softAP()`によりアクセスポイント作成後に`wifi_set_event_handler_cb();`でWiFiのイベントを取得します。

`wifi_handle_event_cb()`ではプローブ要求以外のイベントであれば無視し、プローブ要求してきたクライアントのMACアドレスを取得・比較します。今回でも、このコールバック関数内ではdetectingMAC1, detectingMAC2のフラグを立て、loop()内で処理します。

またWiFiイベントとして以下に示すイベントも取得できるようです。

```
(user_interface.hより)

EVENT_STAMODE_CONNECTED = 0,
EVENT_STAMODE_DISCONNECTED,
EVENT_STAMODE_AUTHMODE_CHANGE,
EVENT_STAMODE_GOT_IP,
EVENT_STAMODE_DHCP_TIMEOUT,
EVENT_SOFTAPMODE_STACONNECTED,
EVENT_SOFTAPMODE_STADISCONNECTED,
EVENT_SOFTAPMODE_PROBEREQRECVED,
EVENT_MAX 
```

## IoTとして他のサービスとの連携
### Milkcocoa

```c:
void detectMAC1(char *msg) {
    DataElement elem = DataElement();
    elem.setValue("Event", msg);
    milkcocoa->push(MILKCOCOA_DATASTORE, &elem);
    Serial.println("Done.");
}
```
例えばdetectMAC1()を上記のように変更すれば、milkcocoaとの連携も可能になります。

### Line Notify (**15:42追記**)
```c:
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>

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

const char* host = "notify-api.line.me";
String LINE_TOKEN = "yourtokenishere";
WiFiClientSecure client;

(中略)

void detectMAC1(String msg) {
  LinePost("message=" + msg, LINE_TOKEN);
}

String LinePost(String data, String Token) {
  Serial.println("httpsPost");
  if (client.connect(host, 443)) {
    Serial.println("Connect");
    client.println("POST /api/notify HTTP/1.1");
    client.println("Host: " + (String)host);
    client.println("User-Agent: ESP8266/1.0");
    client.println("Connection: close");
    client.println("Content-Type: application/x-www-form-urlencoded;");
    client.print("Authorization: Bearer ");
    client.println(Token);
    client.print("Content-Length: ");
    client.println(data.length());
    client.println();
    client.println(data);
    delay(10);
    String response = client.readString();
    int bodypos =  response.indexOf("\r\n\r\n") + 4;
    return response.substring(bodypos);
  }
  else {
    return "ERROR";
  }
}
```

Line Notifyのトークン取得後、上記コードにより連携可能となります。

## セキュリティ問題
これまでのプロミスキャスモードと同様に、MACアドレスのみの照合なので**セキュリティ上の問題あり**！どうしよう・・・

しかしWIFI_AP_STAモードを使うことでWiFi接続したままにできるので、他のサービスとの連携が一層スピーディーに行えるようになりました。例えばMilkcocoaへのpush時間は1秒もかからないです。まだ少し問題は残っているものの、外部サービスとの連携も低遅延で行えるようになりました。今後は押下イベント検出にARPが使えないかを検討していこうと思います。


##ToDo
- Line Notify, Twitter, IFTTT, Slack等の連携
- SSID, パスワードでの認証, ARP？　ちょっと難しそう。


##参考文献
今回、以下の記事の内容・アイディアを参考に実装しました。
- ESP8266 > Amazon Dash Buttonの接続先アクセスポイントになる > v0.1 (失敗編): http://qiita.com/7of9/items/89b24045bf5e44f97be4
- Amazon Dash Button から低遅延で鍵を開ける: http://qiita.com/rukihena/items/aed9999a6a4376d68f0b