M5Stack BasicでWiFi GeolocationをMap表示する

M5Stack Basicを使って、WiFiスキャンで得られるMACアドレス情報を元にGoogle Map上に現在位置をプロットするサンプルを作ってみました。
![IMG_4891.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/bf3b0ccd-b7bc-b870-1994-cd5f62d410de.jpeg)

#WiFiスキャンでAPリストを取得する
まずは、WiFi networksをスキャンし、周辺のアクセスポイントを取得します。WiFi.hをインクルードしたうえで`WiFi.scanNetworks();`でWiFiネットワークをスキャンします。戻り値として見つかったWiFiネットワークの数を返します。その後、スキャンされたネットワーク数でループし、channel(i)でチャネル、RSSI(i)で受信レベル、SSID(i)でSSIDを取得できますが、ここでは、MACアドレスを取得したいので、`WiFi.BSSIDstr(i)`を使います。

```wifiscan.c
int n = WiFi.scanNetworks();  //ネットワークをスキャンして数を取得
for (int i = 0; i < n; i++) {
  String bssid = WiFi.BSSIDstr(i);  //BSSID(MACアドレス)を文字列で取得
}
```

#Google Geolocation APIで位置情報を取得
得られたMACアドレスのリストを元に、[Google Geolocation API](https://developers.google.com/maps/documentation/geolocation/intro)で位置情報（緯度経度）を取得します。
位置情報の取得は、下記のURLにPOSTすることになります。
`https://www.googleapis.com/geolocation/v1/geolocate?key=YOUR_API_KEY`
POST時に先ほどのMACアドレスのリストをJSONで渡すことになります。例えば、このようなJSONです。

```reqest_geolocation.json
{
  "considerIp":"false",
  "wifiAccessPoints":[
    {"macAddress":"AA:AA:AA:AA:AA:AA"},
    {"macAddress":"BB:BB:BB:BB:BB:BB"},
    {"macAddress":"CC:CC:CC:CC:CC:CC"}
  ]
}
```
ArduinoでJSONを取り扱うには[ArduinoJson](https://arduinojson.org/)が便利です。

##ArduinoJsonライブラリのインストール
ArduinoJsonライブラリを使用するためには、Arduino IDEのライブラリマネージャでAruinoJsonをインストールしておく必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/d642e92c-197a-c8b3-84ee-38ef1de66e64.png)

##JSONシリアライズ
ArduinoJsonでは、メモリ消費を極力抑えるために生成するJSONのサイズを指定しておく必要があります。上記のように3つのMACアドレスをJSONコードにする場合に必要なサイズを調べる必要がありますが、[ArduinoJson Assistant](https://arduinojson.org/v6/assistant/)を使いましょう。下図のように、Input欄に想定するJSONコードを入力すると、右側のMemory pool sizeにメモリ確保の記載例が出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/f05505aa-2d50-45a3-ed69-9a77e19db84e.png)
今回の場合、3つの配列がそれぞれ１つずつの要素を持ち(wifiAccessPoints)、considerIpとあわせて2つの要素になるため、`JSON_ARRAY_SIZE(3) + 3*JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)`分の確保が必要になります。更に各要素の文字数を想定し、追加で121バイトを確保しておく必要があります。
下記にJSONシリアライズのサンプルを記載します。（あまり多くのMACアドレスを送るとコスト的にも無駄な場合もありますので、MAX_APで指定しておきます）

```jsonserialize.c
const int MAX_AP = 3
int n = WiFi.scanNetworks();
const size_t capacity = JSON_ARRAY_SIZE(MAX_AP) + (MAX_AP)*JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2) + 34 + (MAX_AP * 29);
DynamicJsonDocument doc(capacity);

doc["considerIp"] = "false";
JsonArray wifiAccessPoints = doc.createNestedArray("wifiAccessPoints");
for (int i = 0; i < n; i++)  {
  String bssid = WiFi.BSSIDstr(i);
  JsonObject wifiAP = wifiAccessPoints.createNestedObject();
  wifiAP["macAddress"] = WiFi.BSSIDstr(i);
  if (i + 1 == MAX_AP) break;
}
serializeJson(doc, json);
```

##Google Geolocation APIにPOST
リクエストJSONが出来上がれば、Google Geolocation APIにPOSTします。
HTTPClientを使ったサンプルは以下の通りです。json_requestは先ほどシリアライズしたJSONコードです。

```google_geolocation.c
HTTPClient http;
http.begin("https://www.googleapis.com/geolocation/v1/geolocate?key=YOUR_API_KEY");
int status_code = http.POST(json_request);
if (status_code == 200) {
  String json_response = http.getString();
  const size_t capacity = 2*JSON_OBJECT_SIZE(2) + 30;
  DynamicJsonDocument doc(capacity);
  deserializeJson(doc, json_response);
  float lat = doc["location"]["lat"];
  float lng = doc["location"]["lng"];
  float accuracy = doc["accuracy"];
}
http.end();
```
ここで、またしても、ArduinoJsonを使って、Google Geolocation APIから帰ってきたJSONをデシリアライズしています。
これで、緯度経度（と精度）が取得できました。

#Google Static Mapで現在位置をプロットする
最後にGoogle Map上に位置をプロットすることになりますが、M5Stackの画面上に静的画像として描画したいので、**[Map Static API](https://developers.google.com/maps/documentation/maps-static/intro)**を使います。このAPIは下記URLに必要なパラメータを渡せば、jpeg等の画像ストリームを取得できます。`https://maps.googleapis.com/maps/api/staticmap?[parameters]&key=YOUR_API_KEY`
指定可能なパラメータは、下記のようなものがあります。([参考](https://maps.multisoup.co.jp/blog/2029/))

|パラメータ|説明|例|
|:--------|:---|:---|
|center|地図の中心座標。「緯度,経度」または「住所」「建物名」なども指定可能です。|35.5000,136.5000、六本木ヒルズ|
|zoom|マップのズームレベルを決定します。1~22を指定できます。|15|
|size|地図画像のサイズ（幅×高さ）を決定します。M5Stackのディスプレイを考えると320x240が最適です。|320x240|
|format|画像の形式を決定します。gif、jpg、png を指定できます。|jpg|
|maptype|地図のタイプを決定します。roadmap, satellite, hybrid, terrain を指定できます。|roadmap|
Static Mapの取得に成功すれば、**getStreamPtr()**で画像ストリームを読み込んでいき、最後に**M5.Lcd.drawJpg(...)**で画面描画をすることとなります。
以下にサンプルを示します。

```drawMap.c
HTTPClient http;
http.begin(url); //urlはパラメータ込みのGoogle Static APIへのURL
int status_code = http.GET();
if (status_code == 200) {
  int len = http.getSize();
  if (len > 0) {
    WiFiClient * stream = http.getStreamPtr();
    // read all data from server
    uint8_t* p = buff;
    int l = len;
    while (http.connected() && (l > 0 || len == -1)) {
      // get available data size
      size_t size = stream->available();
      if (size) {
        int s = ((size > sizeof(buff)) ? sizeof(buff) : size);
        int c = stream->readBytes(p, s);
        p += c;
        if (l > 0) {
          l -= c;
        }
      }
    }
  }
  M5.Lcd.drawJpg(buff, len);
}
http.end();
```
M5StackのdrawJpg関数については、[こちら](https://github.com/m5stack/m5-docs/blob/master/docs/ja/api/lcd.md#drawjpg)を参考にしてください。

#サンプルコード
今回の全サンプルコードは[こちら(GitHub)](https://github.com/hibigaku/m5stack-wifimap/blob/master/m5stack-wifimap.ino)上にあげておきます。Google Static Map用のAPI KeyやWiFi接続用のSSID、キーはご自身の環境にあわせてください。
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172313/d6ad10bc-75ba-8124-14d4-f144657c6473.png)](https://twitter.com/ghibi)