SORACOM Orbit試してみた 〜SORACOM LTE-M Button for Enterprise編〜

続編です。(時間が経ってしまい申し訳ないです。)

---

前回、[SORACOM Orbit試してみた 〜SORACOM GPS マルチユニット編〜](https://qiita.com/keni_w/items/de0f9ca67af8be315f93)では、
サンプルを使って、一通り動作確認できました。

次は、
C++のサンプルを直す形で、
SORACOM LTE-M Button for Enterprise
で使えるようにしてみたい思います。

ちなみに、RustにはSORACOM LTE-M Button for Enterprise対応のサンプルがあります。

## どうなったか。
Orbit登場までも、Lambda側で、SORACOM APIもしくはSORACOM CLIのLambda Layerを使って、SIMの名前やタグなどの情報を取得することができました。
その際は、以下のような実装を行う必要がありました。
1. 認証キー(もしくは他の手段)で、トークンを取得
2. トークンをパラメータに、SIM情報を取得

しかし、Orbit側で、名前やタグにセットされた値を取得することができるので、
この処理が不要になりました。

- Orbit登場前
![スクリーンショット 2020-08-18 10.20.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/c6677b5a-0a98-260b-ac3a-e073f50377a1.png)



- Orbit登場後
<img width="763" alt="処理流れ(Orbit登場後)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/21a29347-1581-ab28-0541-f03bb6c7be8d.png">


となりました。

Lambda側のコード量としては、約半分（SORACOM LTE-M Button powered by AWS用の実装もあったり、いろいろ変えたのもありますが）になり、環境変数にKMSで暗号化した認証キーを保存していましたが、それも不要になりました。

処理が減ったことで、初回起動時間としては、以下のようになっています。

- Orbit登場前
<img width="1276" alt="Lambdaの実行時間等(Orbit登場前)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/74d2d454-22db-a209-ce81-f5d3f75fffd1.png">
- Orbit登場後
<img width="1245" alt="Lambdaの実行時間等(Orbit登場後)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/637a7323-51ba-d045-dbad-bab319f61605.png">

以前、Lambdaの呼び出しおよび実行に成功しているのに、ボタンが赤点滅することあったのですが、
初回起動時などで、2秒ぐらい掛かっているときに起きることがありました。
聞いたところは、認証で時間かかると。Funk側がタイムアウトしてしまっているのではということでしたが、
Lambda側の処理がスッキリしたので、今後はこういうこともなさそうです。

## ソース
### Orbit側
ベースはSORACOM GPSマルチユニットのものです。

```c++
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <string>

#include <emscripten.h>
#include "soracom/orbit.h"

#include "nlohmann/json.hpp"
using nlohmann::json;

int32_t uplink_body();

extern "C" {

EMSCRIPTEN_KEEPALIVE
int32_t uplink() {
    soracom_log("hello, orbit! for IoT Button Edition.\n");

    return uplink_body();
}

}

std::string get_tag(const std::string& tag_name) {
    const char* tag_value = NULL;
    size_t tag_value_len = 0;
    int32_t err = soracom_get_tag_value(tag_name.c_str(), tag_name.size(), &tag_value, &tag_value_len);
    if (err < 0) {
        return "";
    }

    return std::string(tag_value);
}

std::string get_source_item(const std::string& name) {
    const char* value = NULL;
    size_t value_len = 0;
    int32_t err = soracom_get_source_value(name.c_str(), name.size(), &value, &value_len);
    if (err < 0) {
        return "";
    }

    return std::string(value);
}

double deg2rad(double deg) {
    return deg * (M_PI / 180);
}

double calc_distance_in_km(double lat1, double lon1, double lat2, double lon2) {
    double r = 6371; // Radius of the earth in km
	double d_lat = deg2rad(lat2 - lat1);
	double d_lon = deg2rad(lon2 - lon1);
	double a = sin(d_lat/2)*sin(d_lat/2) + cos(deg2rad(lat1))*cos(deg2rad(lat2))*sin(d_lon/2)*sin(d_lon/2);

	double c = 2 * atan2(sqrt(a), sqrt(1-a));
	return  r * c;
}

double parse_double(const std::string& s, double default_value) {
    char *p_end;
    double d = strtod(s.c_str(), &p_end);
    if (*p_end != NULL) {
        return default_value;
    }
    return d;
}

int32_t uplink_body() {
    const char* buf = NULL;
    size_t siz = 0;
    int32_t err = soracom_get_input_buffer_as_string(&buf, &siz);
    if (err < 0) {
        return err;
    }

    soracom_log("received data: %s\n", buf);

    json j = json::parse(buf);
    soracom_release_input_buffer(buf);

    std::string imsi = get_source_item("resourceId");

    soracom_log("imsi = %s\n", imsi.c_str());
    std::string name = get_tag("name");
    std::string center_lat_str = get_tag("center_lat");
    std::string center_lon_str = get_tag("center_lon");
    std::string radius_str = get_tag("radius");

    std::string employee_code = get_tag("employee_code");

    j["name"] = name;
    j["employee_code"] = employee_code;
    j["imsi"] = imsi;
    j["inside_area"] = 0;

    const char* userdata = NULL;
    size_t ud_siz = 0;
    err = soracom_get_userdata_as_string(&userdata, &ud_siz);
    if (err < 0) {
        return err;
    }
    soracom_log("userdata: %s\n", userdata);
    j["user_data"] = std::string(userdata);
    soracom_release_userdata(userdata);

    std::string output = j.dump();
    soracom_set_json_output(output.c_str(), output.size());

    return 0;
}

```

### Lambda側
ベースは
[SORACOM Funkを使ってみた。](https://qiita.com/keni_w/items/4b511f89c8dbd259a78d)
で作成したものです。

```js
'use strict';
const moment = require('moment-timezone');
const AWSXRay = require('aws-xray-sdk')
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
AWS.config.update({ region: 'ap-northeast-1' });
const kms = new AWS.KMS();
AWSXRay.captureHTTPsGlobal(require('https'))
AWSXRay.capturePromise();
const axios = require('axios');

const encryptedSlackWebHookUrl = process.env['SLACK_WEBHOOK_URL'];
let decryptedSlackWebHookUrl;

const clickTypeNameJp = {
  1: 'クリック',
  2: 'ダブルクリック',
  3:'長押し'
}

/**
 * 環境変数復号化
 * @param {*} decryptedEnvKey 
 */
async function decryptedEnv(decryptedEnvKey) {
  const data = await kms
    .decrypt({
      CiphertextBlob: Buffer.from(process.env[decryptedEnvKey], 'base64'),
    })
    .promise();
  return String(data.Plaintext);
}

/**
 * データ取得
 * @param {*} event 
 */
async function getData(event, context) {

  const clickType = event.clickType;
  const clickTypeName = clickTypeNameJp[`${clickType}`];
  const name = event.name;
  const batteryLevel = event.batteryLevel;
  const locationQueryResult = context.clientContext.custom.locationQueryResult;
  let locationLat = null;
  let locationLon = null;
  if (locationQueryResult == 'success') {
    locationLat = context.clientContext.custom.location.lat;
    locationLon = context.clientContext.custom.location.lon;
  }

  return {
    clickType,
    clickTypeName,
    name,
    batteryLevel,
    locationQueryResult,
    locationLat,
    locationLon,
  };
}

/**
 * Slack POST
 * @param  {[type]} data [description]
 * @param  {[type]} isError [description]
 * @return {[type]}       [description]
 */
async function postSlackMessage(data) {
 
  console.log('slack Call.' + JSON.stringify(data));

  let messageArray = [];
  messageArray.push(`ボタン ${data.name} が${data.clickTypeName}されました。`);
  let battryLevelInfo = '電池残量は十分です。'
  if (data.batteryLevel < 0.3) {
    battryLevelInfo = 'そろそろ電池交換の時期です。'
  }
  const batteryLevelPercent = data.batteryLevel * 100;
  messageArray.push(`バッテリー残量は${batteryLevelPercent}です。${battryLevelInfo}`);
  
  // 位置情報付与
  if (data.locationQueryResult === 'success') {
    const mapUrl = 'https://www.google.com/maps?q=,';
    messageArray.push('ボタンの位置は');
    messageArray.push(`https://www.google.com/maps?q=${data.locationLat},${data.locationLon}`);
  }

  const message = messageArray.join('\n');
  // リクエスト設定
  const payload = {
    text : message
  };
  // メッセージ送信
  try {
    const res = await axios.post(decryptedSlackWebHookUrl, JSON.stringify(payload));
    if (res.data === 'ok') {
      return true;
    } else {
      return false;
    }
  } catch (error) {
    console.log('Slack Post Error: ' + error);
    return false;
  }
};

module.exports.index =  async(event, context)  => {
  console.log(JSON.stringify(event, 2));
  console.log(JSON.stringify(context, 2));

  decryptedSlackWebHookUrl = await decryptedEnv('SLACK_WEBHOOK_URL');
  const data = await getData(event, context);

  let isError = false;
  if (!await postSlackMessage(data)) {
    isError = true;
  }

  if (!isError) {
    return {
      statusCode: 200,
      body: JSON.stringify(
        {
          message: 'All Action Successful!',
          input: data,
        },
        null,
        2
      ),
    };
  } else {
    return {
      statusCode: 500,
      body: JSON.stringify(
        {
          message: 'Action Failed!',
          input: data,
        },
        null,
        2
      ),
    };
  }
};
```

## まとめ
今回のサンプルでは、タグから名前の他にemployee_code(社員番号)を取得して、Lambda側に渡してます（今の所、渡しただけで使ってないんですw）、
ボタン＝ユーザーというケースに、その紐付けを管理するためには、別で管理(DynamoDBに格納とか)する、APIで取得するなど、しないといけなかったと思いますが、その辺が楽になったのではないでしょうか。
どこまでタグに格納して良いんだ？っていう話にもなりそうですが。。。

## 次回予告
SORACOM Discovery 2020 Onlineのナイトイベントで行われたソラコム ウルトラクイズで、
大人気なく、全力で答えてしまい、「IoT 体験キット ～磁気センサー～」をゲットしたので、
改めて、[【IoT DIY レシピ】IoT 体験キット ～磁気センサー～ で作る「ドアの開閉モニタリング」](https://soracom.github.io/iot-recipes/door-monitoring-by-iot-starter-kit-magnetic-sensor/#0)をやってみます。
そして、レシピで出てくるHosted Funkと同等の仕組みをOrbitとLambdaで実装したいと思います。