Mozilla Location Serive を使って周囲の WiFi AP の一覧から位置情報を取得する

macOSの`airport`コマンドで周囲のWiFiをスキャンして、得られたBSSIDを[Mozilla Location Service](https://location.services.mozilla.com/)に送って現在地を取得してみます。
MacBook Pro (13-inch, 2016, Two Thunderbolt 3 ports)で試しました。

最初に断っておきますがMacで位置情報を取得したいなら通常は[CoreLocation](https://developer.apple.com/jp/documentation/UserExperience/Conceptual/LocationAwarenessPG/CoreLocation/CoreLocation.html)を使うべきですし、ブラウザから位置情報を取得したいなら[Geolocation API](https://developer.mozilla.org/ja/docs/Web/API/Geolocation)を使うべきです。
`airport`に限らずBSSIDの一覧を取得できるコマンド（Linuxならnmcliなど）とMozilla Location Serviceを組み合わせると、OSを問わずGPSなしで位置情報を取得できます。


### `airport`で周囲のWiFiをスキャンする

`airport`コマンドは`/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport`にあります。
なんでパスが通っていないのかは長年の疑問です。

```sh:コマンド
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s
```

```:実行結果
                            SSID BSSID             RSSI CHANNEL HT CC SECURITY (auth/unicast/group)
                 pr500m-9ec840-2 xx:xx:xx:xx:xx:xx -88  11      Y  JP WPA2(PSK/AES/AES) 
                 elecom5g-CBCA28 xx:xx:xx:xx:xx:xx -91  116     Y  US WPA2(PSK/AES/AES) 
                 A8A795523D8B-5G xx:xx:xx:xx:xx:xx -87  100     Y  JP WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                  aterm-25d691-a xx:xx:xx:xx:xx:xx -37  36      Y  -- WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                  aterm-c1ff43-g xx:xx:xx:xx:xx:xx -81  11      Y  JP WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                penguin piyopiyo xx:xx:xx:xx:xx:xx -81  9       Y  -- WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                W04_54B121E2BF7B xx:xx:xx:xx:xx:xx -46  9       Y  -- WPA(PSK/AES,TKIP/TKIP) WPA2(PSK/AES,TKIP/TKIP) 
                  リビング.o xx:xx:xx:xx:xx:xx -70  6       Y  -- NONE
                  aterm-58c56b-g xx:xx:xx:xx:xx:xx -78  4       Y  JP WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                   ssw-pc-a1a9c9 xx:xx:xx:xx:xx:xx -81  1       Y  -- WPA(PSK/AES/AES) WPA2(PSK/AES/AES)
```

実際は上記コマンドに `| sed 's/..:..:..:..:..:../xx:xx:xx:xx:xx:xx/g'` と続けてBSSIDを伏せ字にしています。


### 1行目を削る

1行目はいらないので `sed -n '2,$p'` で削ります。

```sh:コマンド
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s |
sed -n '2,$p'
```

```:実行結果
                   ssw-pc-a1a9c9 xx:xx:xx:xx:xx:xx -80  1       Y  -- WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                  aterm-58c56b-g xx:xx:xx:xx:xx:xx -83  4       Y  JP WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                  4CE67673D86F-1 xx:xx:xx:xx:xx:xx -89  6       Y  -- WPA(PSK/AES/AES) 
                  aterm-c1ff43-g xx:xx:xx:xx:xx:xx -89  11      Y  JP WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                 elecom5g-CBCA28 xx:xx:xx:xx:xx:xx -90  116     Y  US WPA2(PSK/AES/AES) 
                 A8A795523D8B-5G xx:xx:xx:xx:xx:xx -85  100     Y  JP WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                  aterm-25d691-a xx:xx:xx:xx:xx:xx -41  36      Y  -- WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                penguin piyopiyo xx:xx:xx:xx:xx:xx -83  9       Y  -- WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
                W04_54B121E2BF7B xx:xx:xx:xx:xx:xx -45  9       Y  -- WPA(PSK/AES,TKIP/TKIP) WPA2(PSK/AES,TKIP/TKIP) 
                  リビング.o xx:xx:xx:xx:xx:xx -68  6       Y  -- NONE
                 aterm-cba967-gw xx:xx:xx:xx:xx:xx -84  3       N  JP WEP
                  aterm-cba967-g xx:xx:xx:xx:xx:xx -85  3       Y  JP WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
```

### BSSIDとRSSIだけ抜き出す

続いて、BSSIDとRSSIだけ抜き出します。
`sed 's/^.*\(..:..:..:..:..:..\)  *\(-[0-9]\{1,3\}\).*$/\1 \2/'`とやったらできました。
正規表現がなかなかおぼえられないのでいつも[ここ](https://qiita.com/richmikan@github/items/b6fb641e5b2b9af3522e)を見ています。

```sh:コマンド
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s |
sed -n '2,$p' |  # 1行目を削除する
sed 's/^.*\(..:..:..:..:..:..\)  *\(-[0-9]\{1,3\}\).*$/\1 \2/'  # BSSIDとRSSIだけ抜き出す
```

```:実行結果
xx:xx:xx:xx:xx:xx -87
xx:xx:xx:xx:xx:xx -85
xx:xx:xx:xx:xx:xx -88
xx:xx:xx:xx:xx:xx -77
xx:xx:xx:xx:xx:xx -93
xx:xx:xx:xx:xx:xx -83
xx:xx:xx:xx:xx:xx -86
xx:xx:xx:xx:xx:xx -41
xx:xx:xx:xx:xx:xx -48
xx:xx:xx:xx:xx:xx -80
xx:xx:xx:xx:xx:xx -79
xx:xx:xx:xx:xx:xx -80
xx:xx:xx:xx:xx:xx -40
xx:xx:xx:xx:xx:xx -22
```

### JSONに変換する

[Geolocate API](https://mozilla.github.io/ichnaea/api/geolocate.html)のリクエストボディは次のようなJSONです。

```json:GeolocateAPIリクエストボディの例
{
    "wifiAccessPoints": [{
        "macAddress": "01:23:45:67:89:ab",
        "signalStrength": -51
    }, {
        "macAddress": "01:23:45:67:89:cd"
    }]
}
```

さきほどのSSIDの一覧をJSONに変換します。

```sh:コマンド
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s |
sed -n '2,$p' |
sed 's/^.*\(..:..:..:..:..:..\)  *\(-[0-9]\{1,3\}\).*$/\1 \2/' |
awk '{printf("{\"macAddress\": \"%s\", \"signalStrength\": %s}\n", $1, $2)}' |  # BSSIDとRSSIの組をJSONのオブジェクトに変換する
awk 'BEGIN{printf "["} {if(NR != 1){printf ","};printf $0} END{printf "]"}' | # オブジェクトを配列にまとめる
awk '{printf("{\"wifiAccessPoints\": %s}\n", $0)}'  # 外側のJSONを付け加える
```

```:実行結果
{"wifiAccessPoints": [{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -81},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -88},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -43},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -82},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -83},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -80},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -52},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -72},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -76},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -82},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -39},{"macAddress": "xx:xx:xx:xx:xx:xx", "signalStrength": -22}]}
```

### curlでHTTPリクエストを行う

[curlはリクエストボディの内容を標準入力から受け取れる](https://qiita.com/miminashi/items/f48cd4c25b06fcab5ea3#request-body-%E3%81%AE%E5%86%85%E5%AE%B9%E3%82%92%E3%83%91%E3%82%A4%E3%83%97%E3%81%8B%E3%82%89%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80)ので、さきほどの結果をcurlに渡して、実際にリクエストを送ってみます。

APIのURLは、`https://location.services.mozilla.com/v1/geolocate?key=<API_KEY>`で、開発中などに試しに使ってみるときはAPI_KEYには`test`といれておけばよいようです。

```sh:コマンド
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s |
sed -n '2,$p' |
sed 's/^.*\(..:..:..:..:..:..\)  *\(-[0-9]\{1,3\}\).*$/\1 \2/' |
awk '{printf("{\"macAddress\": \"%s\", \"signalStrength\": %s}\n", $1, $2)}' |
awk 'BEGIN{printf "["} {if(NR != 1){printf ","};printf $0} END{printf "]"}' |
awk '{printf("{\"wifiAccessPoints\": %s}\n", $0)}' |
curl -H "Content-type: application/json" -d @- https://location.services.mozilla.com/v1/geolocate?key=test
```

```:実行結果
{"location": {"lat": 35.xxxxxxx, "lng": 139.xxxxxxx}, "accuracy": 25.7291953}
```

緯度経度が取得できました。
OpenStreetMapに緯度経度を入力してみたら、`accuracy`で示されているくらいの精度で現在地が表示されました。
これは良い・・・

ついでなのでOSMのページをブラウザで開くところまで自動化してみます。

```sh:周囲のWiFiをスキャンしてGeolocationAPIで緯度経度を取得して現在地をブラウザで表示する
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s |
sed -n '2,$p' |
sed 's/^.*\(..:..:..:..:..:..\)  *\(-[0-9]\{1,3\}\).*$/\1 \2/' |
awk '{printf("{\"macAddress\": \"%s\", \"signalStrength\": %s}\n", $1, $2)}' |
awk 'BEGIN{printf "["} {if(NR != 1){printf ","};printf $0} END{printf "]"}' |
awk '{printf("{\"wifiAccessPoints\": %s}\n", $0)}' |
curl -H "Content-type: application/json" -d @- https://location.services.mozilla.com/v1/geolocate?key=test |
sed 's/^.*"lat": \([0-9][0-9]*\.[0-9][0-9]*\).*"lng": \([0-9][0-9]*\.[0-9][0-9]*\).*$/\1 \2/' |
awk '{printf("https://www.openstreetmap.org/#map=19/%s/%s\n", $1, $2)}' |
xargs open
```