M5Stack BasicでWiFi GeolocationをMap表示する


M5Stack Basicを使って、WiFiスキャンで得られるMACアドレス情報を元にGoogle Map上に現在位置をプロットするサンプルを作ってみました。

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で位置情報(緯度経度)を取得します。
位置情報の取得は、下記の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が便利です。

ArduinoJsonライブラリのインストール

ArduinoJsonライブラリを使用するためには、Arduino IDEのライブラリマネージャでAruinoJsonをインストールしておく必要があります。

JSONシリアライズ

ArduinoJsonでは、メモリ消費を極力抑えるために生成するJSONのサイズを指定しておく必要があります。上記のように3つのMACアドレスをJSONコードにする場合に必要なサイズを調べる必要がありますが、ArduinoJson Assistantを使いましょう。下図のように、Input欄に想定するJSONコードを入力すると、右側のMemory pool sizeにメモリ確保の記載例が出てきます。

今回の場合、3つの配列がそれぞれ1つずつの要素を持ち(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を使います。このAPIは下記URLに必要なパラメータを渡せば、jpeg等の画像ストリームを取得できます。https://maps.googleapis.com/maps/api/staticmap?[parameters]&key=YOUR_API_KEY
指定可能なパラメータは、下記のようなものがあります。(参考)

パラメータ 説明
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関数については、こちらを参考にしてください。

サンプルコード

今回の全サンプルコードはこちら(GitHub)上にあげておきます。Google Static Map用のAPI KeyやWiFi接続用のSSID、キーはご自身の環境にあわせてください。