SORACOMひげボタンを使った追跡装置の実装方法

# はじめに
こちらは[SORACOM Advent Calendar 2020](https://qiita.com/advent-calendar/2020/soracom) 12日目の記事です。
初のアドベントカレンダー挑戦です。
超ハイレベルな記事が多い中、稚拙な内容で大変恐縮ではありますが、よろしくお願いいたします。

本記事は [SORACOM UG Online #2](https://soracomug-tokyo.connpass.com/event/192559/) のLTで発表した装置の実装方法について説明しています。
イベントの様子、内容については以下をご覧ください。
- [イベントの動画（YouTube）](https://www.youtube.com/watch?v=4cJZbdSYisw&t=6097s)
- [発表資料（Speaker Deck）](https://speakerdeck.com/ozk009/hige-button-chasing-system)

# 概要
2019年4月にSORACOMの [LTE-M Button Plus](https://soracom.jp/products/gadgets/button_plus/)  （通称ひげボタン）が発売され、同年8月から [簡易位置測位機能](https://dev.soracom.io/jp/docs/location_service/) が提供されています。
これを使って盗難対策（追跡装置）を作ってみました。

## うごき
装置が移動を検知すると位置情報をスマホ（Slack）に通知します。
![function.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/6a18a20d-f82f-ff50-ac30-35bbd047e56f.png)

## 構成図
システム構成は以下となります。
![system.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/5a7b5a3f-7388-4f8d-c119-ea0590e77966.png)
1. ひげボタンが移動を検知するとSORACOM Funkに通知
2. SORACOM FunkからAWS Lambdaを呼び出し
3. AWS LambdaはSORACOM APIを使い、ボタンに割り当てられた名前を取得し、SlackのIncoming Webhooksを叩く
4. 携帯のSlackに通知される

## 移動の検知
移動の検知は [Omronの振動（転倒）センサー](https://www.fa.omron.co.jp/products/family/2939/) を使います。
取り付けはこんな感じです。
![assemble.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/634ae34a-e708-484b-8de4-8effe778ab38.png)
屋外使用のため、[タカチの防水プラボックス](https://www.takachi-el.co.jp/products/GA) に入れました。

また、振動（転倒）センサーはB接点（通常がON、倒すとOFF）なので、通常ボックスを立てた（センサーは横にした）状態にしておき、動かすとONになるようにしておきます。
![setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/494ddb21-2639-34d6-2585-b84bd1401dee.png)

# デモ
簡単ですが、こんな感じで動きます。
![demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/e55930c0-b612-8612-0642-873c46273ffd.png)
1. 装置を持ち上げる
2. スマホのSlackに通知が表示される
3. メッセージのリンクをクリックするとGoogleマップに表示される

あくまで**”簡易”**位置情報なので、数十メートルから数百メートルのズレが出てしまいますが、大まかな追跡はできると思います。

# 実装方法
前置きが長くなりましたが、実装方法です。

以下の順で設定します。
1. 事前準備
2. SORACOM FunkからSlack通知
3. SORACOM追加設定
4. Lambdaの設定

## 1. 事前準備
今回、ボタンに設定された名前を取得する機能を実装するため、SORACOM APIを使えるようにします。
[こちら（拙稿）](https://qiita.com/ozk009/items/1e399b55b73f1341dbfd) を参考にSORACOM APIを使えるようにしてください。

## 2. SORACOM FunkからSlack通知
続いてSORACOM FunkからSlackに通知する仕組みを作ります。
[ソラコムさんのサンプル](https://dev.soracom.io/jp/start/funk_aws_lambda/) に沿ってしてみてください。

## 3. SORACOM追加設定
LambdaからAPIを呼ぶため、以下の手順でSAMの設定を追加します。

(1)  SORACOMコンソール画面で右上のIDボタン、「セキュリティ」をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/7ab62050-7100-89f2-24af-5297ca0d2efc.png)  
(2) 「ユーザーの追加」をクリックする。
(3) 任意の「名前」、「概要」を入力し、「作成」をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/a8f6ed75-d37c-563e-20c9-d5a385ce76dd.png)
(4) 「権限設定」タブで以下のJSONを指定し、保存する。  

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

(5) 「認証設定」タブで「認証キーを生成」をクリックする。
ここで取得する***認証キーID***と***認証キーシークレット***をメモしておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/f9cf2529-6e3e-eae6-93da-1ff6d82a4a6c.png)

## 4. Lambdaの設定
2.で設定したLambdaの環境変数に先ほど取得したSAMの***認証キーID***を「SORACOM_KEY」、***認証キーシークレット***を「SORACOM_KEY_ID」という名前で追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/4d386068-dae4-11b2-f944-62e42125afed.png)
環境変数が既存の「SLACK_URL」とあわせて3つとなります。

続いて、コードを以下に書き換えます。

```Javascript
const https = require('https');
const url = require('url');
const slackUrl = process.env.SLACK_URL;
const soracomKey = process.env.SORACOM_KEY;
const soracomKeyId = process.env.SORACOM_KEY_ID;

const initialize = (event, context) => {
    return new Promise((resolve) => {
        const stash = {
            bat: 0,
            imsi: '',
            lat: 0,
            lon: 0,
            btn: '不明'
        };

        stash.bat = event.batteryLevel;
        stash.imsi = context.clientContext.custom.imsi;
        stash.lat = context.clientContext.custom.location.lat;
        stash.lon = context.clientContext.custom.location.lon;

        console.log(stash);
        resolve(stash);
    });
};

const getButtonName = (stash) => {
    return new Promise((resolve, reject) => {
        var soracomApi = require('soracom_api');
        var soracom = new soracomApi({authKeyId: soracomKey,authKey: soracomKeyId});

        soracom.get('/subscribers/' + stash.imsi,function(err, res){
            console.log({err:err,res:res});
            if (err) {
                console.log('Gadgets API Error: ' + err);
                reject(err);
            } else {
                stash.btn =  res.tags.name;
                resolve(stash);
            }
        });
    });
};

const postSlack = (stash) => {
    return new Promise((resolve, reject) => {
        var slackReqOptions = url.parse(slackUrl);

        slackReqOptions.method = 'POST';
        slackReqOptions.headers = { 'Content-Type': 'application/json' };
        var payload = {
            "text": stash.btn + "が移動されました。\n" +
            'バッテリー残量：' + stash.bat,
            "attachments": [
                {
                    "title": "簡易位置情報",
                    "color": "#34cdd7",
                    "text": `<https://www.google.com/maps?q=${stash.lat},${stash.lon}>`,
                    "mrkdwn_in": ["text"]
                }
            ]
        };

        var body = JSON.stringify(payload);
        slackReqOptions.headers = {
            'Content-Type': 'application/json',
            'Content-Length': Buffer.byteLength(body),
        };

        var req = https.request(slackReqOptions, function(res) {
            if (res.statusCode === 200) {
                console.log('Posted to slack');
                resolve(stash);
            } else {
                console.log('Slack API Error: ' + res.statusCode);
                const err = {
                    "statusCode": res.statusCode
                }
                reject(err)
            }
        });
        req.write(body);
        req.end();
    });
}

exports.handler = (event, context, callback) => {
    console.log('event: %j', event);
    console.log('context: %j', context);
    
    initialize(event, context)
        .then(getButtonName)
        .then(postSlack)
        .then(callback.bind(null, null))
        .catch(callback);
};
```

以上で完了となります。
ボタンを押してみて、通知が届くかを確認してください。

# さいごに
初のアドベントカレンダー、めちゃくちゃ大変でした。
私は元々プログラマーだったのですが、かなりブランクがある"なんちゃってエンジニア”です。
自分用の試作は適当にコード書くのですが、今回はさすがに緊張感持って書き直しました。
至らぬところがあるかと思いますが、お気づきの点がございましたらご指摘いただけると幸いです。

また、今回の装置開発、プログラム（特にSIMに割り当てた名前の取得・非同期処理の対応）は[Kenichiro Wada さんの記事](https://qiita.com/keni_w/items/9e9235dc9d7310062459)を参考にさせていただきました。
この場をお借りして御礼申し上げます。

最後になりますが、このような機会を与えていただき、ありがとうございました。
大変でしたが、良い経験になりました。
中年になって初めてのことができるのは非常にありがたい限りです。

では引き続きアドベントカレンダーをお楽しみください！