続編です。(時間が経ってしまい申し訳ないです。)
前回、SORACOM Orbit試してみた 〜SORACOM GPS マルチユニット編〜では、
サンプルを使って、一通り動作確認できました。
次は、
C++のサンプルを直す形で、
SORACOM LTE-M Button for Enterprise
で使えるようにしてみたい思います。
ちなみに、RustにはSORACOM LTE-M Button for Enterprise対応のサンプルがあります。
どうなったか。
Orbit登場までも、Lambda側で、SORACOM APIもしくはSORACOM CLIのLambda Layerを使って、SIMの名前やタグなどの情報を取得することができました。
その際は、以下のような実装を行う必要がありました。
- 認証キー(もしくは他の手段)で、トークンを取得
- トークンをパラメータに、SIM情報を取得
しかし、Orbit側で、名前やタグにセットされた値を取得することができるので、
この処理が不要になりました。
となりました。
Lambda側のコード量としては、約半分(SORACOM LTE-M Button powered by AWS用の実装もあったり、いろいろ変えたのもありますが)になり、環境変数にKMSで暗号化した認証キーを保存していましたが、それも不要になりました。
処理が減ったことで、初回起動時間としては、以下のようになっています。
- Orbit登場前
以前、Lambdaの呼び出しおよび実行に成功しているのに、ボタンが赤点滅することあったのですが、
初回起動時などで、2秒ぐらい掛かっているときに起きることがありました。
聞いたところは、認証で時間かかると。Funk側がタイムアウトしてしまっているのではということでしたが、
Lambda側の処理がスッキリしたので、今後はこういうこともなさそうです。
ソース
Orbit側
ベースはSORACOM GPSマルチユニットのものです。
#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を使ってみた。
で作成したものです。
'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 体験キット ~磁気センサー~ で作る「ドアの開閉モニタリング」をやってみます。
そして、レシピで出てくるHosted Funkと同等の仕組みをOrbitとLambdaで実装したいと思います。