Wio LTEで遊んでみる（その８：外部システムへ連携）

[Grove IoT スターターキット for SORACOM](https://soracom.jp/press/2017102501/)で遊んでみるの続き。

- [初回：環境構築＋Lチカ](https://qiita.com/ryo_naka/items/45fcb686f264f537a493)
- [二回目：温湿度センサー](https://qiita.com/ryo_naka/items/57e34c3fadb3414fc6c6)
- [三回目：ボタン＋ブザー](https://qiita.com/ryo_naka/items/6f1420574f13f84c8fa6)
- [四回目：磁気スイッチと超音波距離センサー](https://qiita.com/ryo_naka/items/c433a9c362679cee3d18)
- [五回目：3軸デジタル加速度センサーとGPS](https://qiita.com/ryo_naka/items/5ab8f0374d68f0df7394)
- [六回目：簡易地震計を作ってみる](https://qiita.com/ryo_naka/items/ba528c83ff3614326efa)
- [七回目：簡易地震計を改善する](https://qiita.com/ryo_naka/items/4ed3f014688089552f63)


# 今回の目的
今まではHarvestへ連携していたデータをBeam,AWS Lambdaを使用して別システムへ連携させることを目的とする。
GPSで位置情報が取得できるのでArcGISへ連携してみる。
※可能であればSlackにも通知したい。

~~※2018/08/03時点では未完成~~

# 必要なものを順番に準備
## 連携先(ArcGIS)を作成
https://developers.arcgis.com/
まずアカウントを作る。
※既にあるのであれば不要。
<img width="586" alt="AGOL_01.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/c72a5220-47a9-a5eb-e267-2c659788abb9.png">


情報を流し込むためのレイヤーを作成。
<img width="828" alt="AGOL_02.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/a516c5f2-9a79-184f-6f3f-cb5cc86f81ef.png">

<img width="486" alt="AGOL_03.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/1bf40b56-07b2-0b2a-6847-26dd71f12299.png">


[Details]
名前とタグを適当に設定

<img width="656" alt="AGOL_04.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/03e8eb6b-2943-1d3f-cab2-6e65721a5972.png">

[Geometry]
今回は点のデータなのでpointを指定。
測地系(Spatial Reference)はたぶん世界測地系なので4326のままでOK
<img width="671" alt="AGOL_05.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/8e1acec5-ec1b-16d4-2b2b-18e00c162272.png">


[Fields]
今回使用する項目を定義する
とりあえずは以下を想定
imei：SIMのIMEI：データ的には数字列なのだが、Longだと桁数的にはいらないので文字列にしておく
time：取得日時用？
lat：緯度
lon：経度
mode：送信モード：モードの設定に対してコード値ドメインも設定
temp：温度
humi：湿度
g_ave：揺れ関係のデータ

other_string：後で追加したくなったとき用(文字列)
other_double：後で追加したくなったとき用(数字)
other_date：後で追加したくなったとき用(日時)
<img width="638" alt="AGOL_07.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/e97b0c16-560a-4e32-b98e-ca625a27401b.png">

<img width="621" alt="AGOL_07_01.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/6fbc3e77-fd57-4d6c-12c1-39d972e78dc2.png">

[Settings]
ShareLayer
セキュリティ的なものも考えるとPrivateがいいんだが、外から呼び出す際にトークンを生成したりするのがめんどくさいのでとりあえずPublicにしてしまう。

Permissions Settings
Publicにしている代わりというわけではないが、Addのみにしておく。

<img width="216" alt="AGOL_08.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/f09d7f81-a9fb-9b51-a406-4e6ae08c0be1.png">
<img width="207" alt="AGOL_08_01.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/71a354ae-580a-0a9a-ecc0-fad41401bc4e.png">


で、CreateLayer


作成したレイヤーがちゃんとできているかを確認する。
レイヤーからWebMapを作成。
[編集]を押下して、New Featureで追加できることを確認。

<img width="219" alt="AGOL_08_02PNG.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/f69e0231-f0a7-8034-ac11-9dd5f1accca2.png">


これでArcGIS側の準備はOK

作成したフィーチャレイヤーのServie URLを控えておく。
<img width="825" alt="AGOL_09.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/934c0d30-19ee-c036-74a9-9660eabc6dc3.png">
https://[xxxxxxxx].arcgis.com/[XXXXXXXXXXXX]/arcgis/rest/services/fromsoracom_01/FeatureServer

これの後ろに[/0/applyEdits]をつけ、以下のDataをPOSTしてあげると登録できる。
※この段階で一度試してみておくとよい。


+ URL：
https://[xxxxxxxx].arcgis.com/[XXXXXXXXXXXX]/arcgis/rest/services/fromsoracom_01/FeatureServer/0/applyEdits

+ Content-Type：application/x-www-form-urlencoded

+ Data : "adds=" + json_q + "&f=pjson"

json_qの中身はこんな感じ。

    {
        "geometry":{
            "x":140,
            "y":35,
            "spatialReference":{"latestWkid":4326,"wkid":4326}
        },
        "attributes":{
            "imei":1234,
            "mode":0,
            "temp":30,
            "humi":60,
            "g_ave":2,
            "lon"：140,
            "lat"：35
        }
    }

## SORACOM Beam→AWS API Gateway →AWS Lambda
上記で単純なPythonからの登録処理はできそうなのでSORACOM→AWS Lambdaの部分を作る

以下を参考にする。
https://dev.soracom.io/jp/docs/aws_guide/

API Gateway → Lambdaの部分までは問題なく完了。
外部からAPIをCallしてArcGISへ登録できることも確認。

ただ、SORACOM Beam → API Gatewayの部分の連携がよくわからない。
どういう形式で飛んでいくことを想定しているのか。。。
POST/PUTのJsonのBodyがそのまま[$input.json('$')]の部分に当てはまるということなのだろうが
UDP経由でBeamに上げHTTPSで転送した場合どうなるのだろう？

具体的にいうと

    int connectId;
    connectId = Wio.SocketOpen("beam.soracom.io", 23080, WIOLTE_UDP);
    if (!Wio.SocketSend(connectId, data)) 
    {
    SerialUSB.println("### SocketSend ERROR! ###");
    goto err_close;
    }

    int length;
    length = Wio.SocketReceive(connectId, data, sizeof (data), RECEIVE_TIMEOUT);
    if (length < 0) 
    {
    SerialUSB.println("### SocketReceive ERROR! ###");
    goto err_close;
    }

という流れの中で
\### SocketReceive ERROR! ###
となる。

以下を読むとBase64で変換されてしまっているのからなのか？
https://dev.soracom.io/jp/beam/send-data-using-beam/#udp2http
変換したものを送って、それを直接読み込もうとしているからエラーになっているということなのか？

ただ、軽く調査した感じだとAWSのLambdaまでデータが行ってなさそう。
※Lambdaの関数が呼び出されてなさそうな感じがする。

で、Open/SendはできているということはSORACOMまでは行っているような気がする。
※でもコンソール画面のエラーログには出ていないので行っていない可能性もある？

わからん。
ハマりかけている気がするのでいったんここで終了。

## 追記
### Lambdaでの処理が間違っていた
Harvestからぶっこ抜いてきたエンコードされているデータをこれを使って検証にしてみることにする。
`[データ]
{"payload":"eyJsYXQiOjAuMDAwMDAwLCJsb24iOjAuMDAwMDAwLCJ0ZW1wIjoyNS43LCJodW1pIjo1OS4wLCJnX2F2ZSI6MC43MTUsIm1vZGUiOjJ9"}`
`[一次処理済みデータ]
{"value":"{\"lat\":0.000000,\"lon\":0.000000,\"temp\":25.7,\"humi\":59.0,\"g_ave\":0.715,\"mode\":2}"}`
※payloadを変換したあとにvalueが入ってる？

結論
base64.b64decodeで変換した結果の使い方が間違っていたっぽい。
辞書で渡してたから変換後もそのままで扱ってたけど、よく考えたらこれの結果は文字列だった。
なのでjson.loadsしてあげればOK

    encoded = event["item"]["payload"]
    receive_data = base64.b64decode(encoded).decode()
    receive_json_data = json.loads(receive_data)
こんな感じ。
payloadの状態でLambdaでテストしてなかったのが間違いだった。テスト大事。
※payloadを変換した結果にvalueというKeyも入っているのかと思ったらそんなことはなかった。

### ダメだった。
が、これでOKかと思ったらそんなことはなかった。
デバイスからだとやっぱり上手くいかず。
やはりAWSまで飛んでいないのか？

一回Harvestへの送信に戻してみる。
特に問題なくできてそう。

で、もう一回Beam。
飛んでこない。
CloudWatch でも確認してみるがやっぱり飛んできてなさそう。

確認のため、BeamからSlackへ飛ばしてみることにする

<img width="169" alt="20180804_slack_01.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/9860c082-0041-efe0-1598-3dd15f49366b.png">

飛ばせてる。
当たり前だけど、SORACOMまでは来ていることが分かる。
※この辺を簡単にグループ設定/変更できるのはSORACOMのいいところだと思う。

### 突然の解決
ふと思い立って「事前共有鍵」の設定を追加。
※これがなんだかよくわからなかったので未設定だった。

<img width="405" alt="20180804_psk_01.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/53b0e3fa-471f-759a-a0d4-3550bd72397b.png">

これでAWSへも送信されるようになった。
AWSへ送信したい場合はこれの設定が不可欠だったのか？

ということでやりたかったことは完成。
ついでに「揺れ」を検知した場合はSlackへの送信するようにしてみた。
※GPSから位置情報が取れなかったとき、適当な位置にポイント作成するようにしてます。

**Lambda**

    import json
    import base64

    AGOL_applyEdits = r"https://[xxxxxxxx].arcgis.com/[XXXXXXXXXXXX]/arcgis/rest/services/fromsoracom_01/FeatureServer/0/applyEdits" 
    Slack_Token = "xoxp-xxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxxxxxxxx"

    def lambda_handler(event, context):
        
        # Base64で変換された結果がpayloadに格納されているのでデコードする
        encoded = event["item"]["payload"]
        receive_data = base64.b64decode(encoded).decode()
        receive_json_data = json.loads(receive_data)

        send_data = CreateAddData(receive_json_data)
        AddFeature(AGOL_applyEdits, send_data)
        
        if(receive_json_data["mode"] == 2):
            to_slack(receive_json_data)

    def CreateAddData(attributes_dic):

        # 測地系設定：固定(WGS1984で設定)
        geometry_dic = {}
        geometry_dic["x"] = attributes_dic["lon"]
        if(0==geometry_dic["x"]):
            geometry_dic["x"] = 140.1
        geometry_dic["y"] = attributes_dic["lat"]
        if(0==geometry_dic["y"]):
            geometry_dic["y"] = 35.1
        spatialReference_dic = {}
        spatialReference_dic["latestWkid"] = 4326
        spatialReference_dic["wkid"] = 4326
        geometry_dic["spatialReference"] = spatialReference_dic

        # パラメータ作成
        devData_dic = {}
        devData_dic["geometry"] = geometry_dic

        # 時間：SORACOMから渡されるUNIX TIMEを使用してもいい。
        from datetime import datetime
        attributes_dic["time"] = datetime.utcnow().strftime('%Y/%m/%d %H:%M:%S')

        devData_dic["attributes"] = attributes_dic

        return devData_dic

    def AddFeature(url, devData_dic):
        # 送信する
        # 属性データの辞書をJSONに変換
        json_str = json.dumps(devData_dic)

        # 属性JSONをURLエンコード
        import urllib.parse
        json_q = urllib.parse.quote(json_str)

        # 送信パラメータを構築
        sendData = "adds=" + json_q + "&f=pjson"

        # 送信(POST)
        headers = {"Content-Type" : "application/x-www-form-urlencoded"}
        req = urllib.request.Request(url, headers=headers, data=sendData.encode('utf-8'))
        with urllib.request.urlopen(req) as response:
            response_body = response.read().decode("utf-8")
            json_dict = json.loads(response_body)
            return json_dict

    def to_slack(receive_json_data):
        import urllib.request, urllib.parse
        params = {
            "token": Slack_Token,
            "channel": "random",
            "text": "ゆれた気がする\n緯度:{}\n経度:{}".format(receive_json_data["lat"],receive_json_data["lon"])
        }
        p = urllib.parse.urlencode(params)
        url = "https://slack.com/api/chat.postMessage?" + p
        with urllib.request.urlopen(url) as res:
        res.read().decode("utf-8")


ArcGISでの表示
<img width="706" alt="20180804_end_01.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/2da6276b-fe54-0218-279d-38626a901663.png">

以下で参照することもできます。
https://rio-naka.maps.arcgis.com/apps/webappviewer/index.html?id=3466ae5585364e98ae7e7abdfa4bfb89

Slackでの通知結果
<img width="276" alt="20180804_end_02.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/106639/53d8d02b-a29f-ebbd-e45c-cccf2e3c509c.png">

## まとめ
とりあえずこれでひとしきりで遊んでみたい連携機能の確認はできたような気がする。
セキュリティ的にはザルなので気を付ける必要はありますが。。

SORACOMのBeam上で複数の送信先に転送ができるといいなぁとも思う。
あと、エラーログ？しかコンソール上では確認できないみたいなので、もうちょっといろいろと参照できると嬉しいかも。Harvestとかだとあまり気にならないが、BeamだとIN/OUTが見たかった。
逆にやりたいことに合わせてグループを作成しておけば、簡単に変更できるというのはGood。