GPSマルチユニットSORACOM Editionと簡易位置情報 - 解決編 -

# これは

[GPSマルチユニットSORACOM Editionと簡易位置情報](https://qiita.com/kkimura/items/84546555777790e9bd80)という私の記事の解決編です。先にこちらをご覧ください。

抜粋してざっくりまとめますと

- [GPSマルチユニット](https://dev.soracom.io/jp/gps_multiunit/what-is-gps_multiunit/)に[plan-KM1のSIM](https://soracom.jp/products/sim/plan-km1/)を挿すことで、室内等GPS情報が取得できない場合でも[簡易位置測位機能](https://dev.soracom.io/jp/docs/location_service/)を利用して位置情報を取得できる
- GPSマルチユニットは、電源が入っている間はできるだけSIMの通信セッションを維持しようとする
- 簡易位置測位機能は「セッション開始時の情報」に基づいて位置を返す
- そのため、GPSマルチユニットが移動しても位置が更新されない
- 困った

ということで、それを解決します。

# セッションをなんとかしよう

問題点は既に分かっており、何をすればいいのかははっきりしています。そう、セッションを切ってやればいいのです。
データを受信した後にセッションを切断してやれば、取得した簡易位置情報は「前回通信を行った場所」となり、移動と比較してある程度短いスパンで通信をしてあげれば十分実用に耐えられると考えられます。

その実装ですが、[SORACOMエンジニアブログ「セッション切断APIの公開とその使いどころ」](https://blog.soracom.jp/blog/2016/07/01/delete-session-api/)にもありますように、セッションを切断するAPIは準備されています。
これを叩くようなLambdaを準備すればいいわけですが、APIを直接叩くのではなくせっかくですのでまだ触ったことのなかった[SORACOM cli Lambda layer](https://dev.soracom.io/jp/docs/cli_guide/)を使ってみようと思います。

# SORACOM cli Lambda layerとは

AWS Lambda layerの公式ドキュメントは[こちら](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html)です。

簡単に言えば、AWS Lambdaの実行基盤をカスタマイズし、任意のライブラリなどを複数のLambda関数間で共有するための仕組みです。
SORACOMからは、SORACOM cliを動かすために必要な環境を揃えたLambda layerが提供されており、これを使うことで**自作のLambda関数の中でSORACOM cliを呼び出す**ことが可能になります。

実際の使い方は[SORACOMエンジニアブログ「SORACOM の利用料金を定期的に Slack に通知する」](https://blog.soracom.jp/blog/2020/01/20/notify-slack-of-soracom-usage/)を参考にします。

# Lambda関数を変更する

前回作成した、Beamで呼び出される関数を変更します。

```json
import json
import os
import subprocess
import sys

def delete_session(auth_key_id,auth_key,imsi):
    cmd = "soracom subscribers delete-session --auth-key-id %s --auth-key %s --imsi %s" % (auth_key_id,auth_key,imsi)

    try:
        result = subprocess.run(cmd,check=True,
                                stdout=subprocess.PIPE, universal_newlines=True,shell=True)
        return result.stdout
    except subprocess.CalledProcessError:
        print('command error', file=sys.stderr)

def lambda_handler(event, context):
    imsi = event['headers']['x-soracom-imsi']
    auth_key_id = os.environ['auth_key_id']
    auth_key = os.environ['auth_key']
    print(delete_session(auth_key_id,auth_key,imsi))
    print(json.dumps(event))
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

コンソールのLayerの編集画面からarnを指定してLayerを追加します。
Layerは2020/6/1現在の最新版であるv0.5.4に合わせてarnを```arn:aws:lambda:ap-northeast-1:717257875195:layer:soracom-cli-052:1```としました。

SORACOMコンソールからユーザを作り、権限を以下の通り設定します。今回はセッションの削除だけ出来ればいいので、それだけが実行できるようにしています。

```json
{
    "statements":[
        {
            "effect":"allow",
            "api":["Subscriber:deleteSubscriberSession"]
        }
    ]
}
```

発行した認証キーのIDをLambdaの実行時環境変数`auth_key_id`に、認証キーシークレットを`auth_id`に設定します。
また、コマンドの実行に少し時間がかかるので、Lambdaの実行時間制限をデフォルトの3秒から30秒程度に伸ばしておきましょう(10秒もあれば十分とは思います)。

# 実行してみよう

できたら早速GPSマルチユニットのボタンをポチッとしてみましょう。

Harvestで確認すると、以下のようなデータが届いています。

|日時|データ|
|:---|:---|
|2020/?/? ??:?2:44|{"value":"{\"lat\":null,\"lon\":null,\"bat\":3,\"rs\":1,\"temp\":28.8,\"humi\":61.6,\"x\":128.0,\"y\":-256.0,\"z\":-832.0,\"type\":1}","$metadata":{"locationQueryResult":"success","location":{"lat":33.AA\*\*\*\*\*\*\*\*\*\*,"lon":130.BB\*\*\*\*\*\*\*\*\*\*\*\*\*\*}}}|

SIMのセッションのログを見ると、その時間にセッションが正常に削除されています。そして、直後に再度セッションが開始しているようで、エリアコードとセルIDも更新されています。

|日時|イベント|エリアコード|セルID|
|:---|:---|:---|:---|
|2020/?/? ??:?2:47|Deleted|49CCC|134DDDDDD|
|2020/?/? ??:?2:48|Created|49CCC|136DDDDDD|

しばらくしてからボタンを押すと、以下のようなデータが届いていました。

|日時|データ|
|:---|:---|
|2020/?/? ??:?9:04|{"value":"{\"lat\":null,\"lon\":null,\"bat\":3,\"rs\":1,\"temp\":30.6,\"humi\":55.8,\"x\":0.0,\"y\":-64.0,\"z\":-960.0,\"type\":0}","$metadata":{"locationQueryResult":"success","location":{"lat":33.EE\*\*\*\*\*\*\*\*\*\*\*\*\*,"lon":130.FF\*\*\*\*\*\*\*\*\*\*\*\*}}}|

無事、次の送信の際は位置情報が更新されてます。

しかし、ボタンを押したときのGPSマルチユニットの挙動がおかしいです。LEDがずっと点滅し、最後にエラーになったように見えます。データは正常に届いてるんですけどね・・・。これは何でしょう？

# Beam/Funkから呼ばれたLambdaでセッションを切断？駄目でしょ？

よく考えたらBeamやFunkでの呼び出しは同期呼び出しです。時系列で示すと、

|タイムシーケンス|処理|備考|
|:----|:----|:----|
|1|GPSマルチユニット通信開始||
|2|SORACOMサービスにデータ送信完了|GPSマルチユニットはACKが返るのを待つ|
|3|Beam/Funk呼び出し||
|4|呼ばれたLambdaの処理完了|セッションを切ってから処理が終了する|
|5|GPSマルチユニットにACKが返る|実際にはセッション切れてるのでACK返ってこずタイムアウト|
|6|GPSマルチユニットがスリープに入る||

となってると想像できます。GPSマルチユニットから見たらACKが返ってくるの待ってるのに通信が切られちゃってるわけですから、そりゃタイムアウトまで待ちますよね・・・。

これを解決するには、セッションを切る処理を後回し(ACKを返した後)にしてやればいいことになります。

# 解決案1：SQSで切断を後回しにする

上記4のLambdaでやっていた切断処理を別のLambda(以下、Lambda-Bとします)に置き換え、SQSを使って遅延呼び出しをするようにします。
時系列で書くと

|タイムシーケンス|処理|備考|
|:----|:----|:----|
|1|GPSマルチユニット通信開始||
|2|SORACOMサービスにデータ送信完了|GPSマルチユニットはACKが返るのを待つ|
|3|Beam/Funk呼び出し||
|4|呼ばれたLambdaの処理完了|終了前にSQSにimsiのデータを登録|
|5|GPSマルチユニットにACKが返る||
|6|GPSマルチユニットがスリープに入る||
|7|SQSのデータからLambda-Bが呼ばれる|セッションが切断される|

SQSのイベントからLambda-Bがすぐに呼ばれ(上記の7が5よりも前になってしまう)問題が出そうですが、SQSには[delay seconds](https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-delay-queues.html)というパラメータがあり、これを指定したメッセージをキューに登録すると、処理側(この場合はLambda-B)には**指定した時間の間はデータがあるように見えない**ようになります。
これを60秒にしておきます。実際はもっと短くてもいいかと思います。

実際のコードは以下のようになります。

1. soracom-delete-sessionというSQSのキューを作っておきます

2. BeamやFunkから呼ばれるLambda-Aは以下の通りです。最初のバージョンにSQSへのキューイングを追加してます。こちらは今まで通りAPI Gatewayから起動しますが、SQSへのアクセス権を付与するのを忘れないようにしましょう。

```python
import json
import boto3

sqs = boto3.resource('sqs')
queue =sqs.get_queue_by_name(QueueName="soracom-delete-session");

def lambda_handler(event, context):
    imsi = event['headers']['x-soracom-imsi']
    print(json.dumps(event))
    
    try:
        response = queue.send_messages(Entries=[{"Id": imsi,"MessageBody": imsi,"DelaySeconds": 60}]);
        print(response)
    except Exception as e:
        print(str(e))    
        
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

3. SQSから呼ばれるLambda-Bは以下の通りです。こちらもSQSへのアクセス権を付与し、SORACOM cliのLambda layerをセットし、実行時間上限を伸ばし、環境変数でauth_idとauth_id_keyを渡します。また、コードの中で渡ってきたSQSのデータは先頭のもの(Record[0])しか見てませんので、SQSトリガーのバッチサイズは1にしておきます。

```javascript
import json
import boto3
import os
import subprocess
import sys

def delete_session(auth_key_id,auth_key,imsi):
    cmd = "soracom subscribers delete-session --auth-key-id %s --auth-key %s --imsi %s" % (auth_key_id,auth_key,imsi)

    try:
        result = subprocess.run(cmd,check=True,
                                stdout=subprocess.PIPE, universal_newlines=True,shell=True)
        return result.stdout
    except subprocess.CalledProcessError:
        print('command error', file=sys.stderr)

def lambda_handler(event, context):
    imsi = event['Records'][0]['body']
    auth_key_id = os.environ['auth_key_id']
    auth_key = os.environ['auth_key']
    print(delete_session(auth_key_id,auth_key,imsi))
    print(json.dumps(event))
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

実際に動作させてみると、データ送信後にすぐにLEDが消え、ACKが返ってきて正常に終了したことが分かります。
そして渡ってきたデータは以下の通り。

|日時|データ|
|:---|:---|
|2020/?/? ??:05:20|{"value":"{\"lat\":null,\"lon\":null,\"bat\":3,\"rs\":0,\"temp\":28.4,\"humi\":63.6,\"x\":0.0,\"y\":0.0,\"z\":-960.0,\"type\":1}","$metadata":{"locationQueryResult":"success","location":{"lat":3\*.\*\*\*\*\*\*\*\*\*\*\*\*\*, "lon":13\*.\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*}}}|

セッションのログは以下の通りです。

|日時|アクション|
|:---|:---|
|2020/?/? ??:06:26|Deleted|
|2020/?/? ??:06:27|Created|

無事、データを送り終わって概ね60秒後にセッションが切断されている事が分かります。



# 解決案2:Beam/FunkではなくFunnelを使ってLambdaの呼び出し自体を非同期にしてしまう

もう1つの方法として、FunnelのAWS IoTアダプタを使うことです。
[SORACOM Funnelの制限](https://dev.soracom.io/jp/funnel/limits/)に書いてあるように、Funnelでのクラウド連携は非同期です。これを逆に利用して、時系列として

|タイムシーケンス|処理|備考|
|:----|:----|:----|
|1|GPSマルチユニット通信開始||
|2|SORACOMサービスにデータ送信完了|GPSマルチユニットはACKが返るのを待つ|
|3|Funnel呼び出し||
|4|GPSマルチユニットにACKが返る||
|5|GPSマルチユニットがスリープに入る||
|6|FunnelからAWS IoTが呼ばれる||
|7|AWS IoTからLambda-Bが呼ばれる|セッションが切断される|

とすることができます。6のFunnelからの呼び出しがどういうタイミングになるかがコントロールは出来ませんが、そこは呼ばれたLambda側で10秒程度スリープするなりしたらOKでしょう(恐らくほとんどのケースではそこまで不要と思います。今回はスリープ無しで実行してみています)。

実際のコードは以下の通りです。

1. IoT CoreとFunnelの設定をします。[公式ドキュメント「SORACOM Funnel AWS IoT アダプターを使用してデータを送信する」](https://dev.soracom.io/jp/start/funnel_aws_iot/)を参照してください。
2. ルールから呼ばれるLambda関数は以下の通りです。最初に作成したバージョンと、imsiの取り方が違うだけです。実行時間上限を伸ばし、Lambda layerを設定し、環境変数でauth_idとauth_id_keyを渡します。

```python
import json
import os
import subprocess
import sys

def delete_session(auth_key_id,auth_key,imsi):
    cmd = "soracom subscribers delete-session --auth-key-id %s --auth-key %s --imsi %s" % (auth_key_id,auth_key,imsi)

    try:
        result = subprocess.run(cmd,check=True,
                                stdout=subprocess.PIPE, universal_newlines=True,shell=True)
        return result.stdout
    except subprocess.CalledProcessError:
        print('command error', file=sys.stderr)

def lambda_handler(event, context):
    imsi = event['imsi']
    auth_key_id = os.environ['auth_key_id']
    auth_key = os.environ['auth_key']
    print(delete_session(auth_key_id,auth_key,imsi))
    print(json.dumps(event))
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

実際に動作させてみると、データ送信後にすぐにLEDが消え、ACKが返ってきて正常に終了したことが分かります。
そして渡ってきたデータは以下の通り。

|日時|データ|
|:---|:---|
|2020/?/? ??:08:37|{"value":"{\"lat\":null,\"lon\":null,\"bat\":3,\"rs\":0,\"temp\":30.1,\"humi\":59.6,\"x\":256.0,\"y\":-768.0,\"z\":-448.0,\"type\":1}","$metadata":{"locationQueryResult":"success","location"{"lat":3\*.\*\*\*\*\*\*\*\*\*\*\*\*\*, "lon":13\*.\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*}}}|

セッションのログは以下の通りです。

|日時|アクション|
|:---|:---|
|2020/?/? ??:08:43|Deleted|
|2020/?/? ??:06:44|Created|

無事、6秒後にセッションが切断されている事が分かります。

# まとめ

今回のまとめです。

- GPSマルチユニットはSIMのセッションを維持しようとするので、移動しても簡易位置情報が更新されない
- データが届くたびに、SORACOM APIを用いてセッションを切断すれば良い
    - 実現にはSORACOM cli Lambda layerを使う
- 届いたデータを処理するBeam/Funkから呼ばれたLambda内で切断すると問題がある
    - 解決案1：SQSで次のLambdaを呼んでそちらで切断する
    - 解決案2：SORACOM Funnelを用いて、Lambdaの呼び出しを非同期にする

SORACOM cli Lambda layerを用いて、AWS LambdaでSORACOM APIを呼び出してセッションを切断することが出来ました。
これで無事GPSマルチユニットにplan-KM1のSIMを挿して簡易位置情報を取れるようになりました！