Raspberry Pi3×SORACOM×AWS IoT×myThingsで勤怠連絡ボタンを作ってみる

# 概要
下記2つの記事のRaspberry Pi3(以後ラズパイ)版。

* [myThingsで勤怠連絡ボタンを作ってみた](https://techblog.yahoo.co.jp/advent-calendar-2016/mythings-attendance-contact-button/)
* [myThingsとAmazon Dash ButtonをAWS経由で繋げてみる](http://qiita.com/yumatsud/items/0771217c9c3efe44594b)

ラズパイでボタンを押したら勤怠連絡が飛ぶようにしてみたいと思います。

# やること
1. ラズパイにスイッチをつける
* ラズパイのスイッチを押すと、SORACOM経由でAWSへ
* SORACOM経由での通信はMQTTで、MQTTSにする部分はSORACOM Beamで吸収
* AWSへの部分はAWS IoTで実現
* AWS IoTからはDynamoDBへデータを保存
* AWS DynamoDBにデータが保存されたら、AWS Lambdaをキック
* AWS LambdaからmyThings Developers経由で勤怠連絡
* 勤怠連絡にはSlack、Y!メールを利用

# 用意するもの
* Raspberry Pi3
* 3G USB ドングル (FS01BU)
* SORACOM Air
* AWS IoT
* AWS DynamoDB
* AWS Lambda
* myThings Developers

# 事前準備
* ラズパイの準備とSORACOMの接続は下記2つの記事を参照
    * [Raspberry Pi 3を購入してからMacでssh接続するまでの道のり](http://qiita.com/yumatsud/items/83732a5be3fe80b1054b)
    * [Raspberry Pi3をSORACOM経由で通信&自動接続に挑戦](http://qiita.com/yumatsud/items/e159d7ffe73de3a65deb)
* myThings Developersの設定は下記の記事を参照
    * [myThingsで勤怠連絡ボタンを作ってみた](https://techblog.yahoo.co.jp/advent-calendar-2016/mythings-attendance-contact-button/)
* AWS DynamoDBは下記記事の「AWS DynamoDBの準備」を参照
    * [myThingsとAmazon Dash ButtonをAWS経由で繋げてみる](http://qiita.com/yumatsud/items/0771217c9c3efe44594b)

# やったこと
上記のやることの1〜6までの作業をやっていく。
順番は若干違いますが、一通りやってみました。

## AWS IoTの準備
AWS IoTで所々行ったことをメモしておきます。

### Thingの作成
* AWS IoTコンソールの「Registry」から「Register thing」をクリック
<img width="1434" alt="スクリーンショット 2017-01-07 16.27.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/5ef37481-3945-d118-f5f0-cce2d6e622a2.png">

* Thing名を入力して「Create thing」をクリック
<img width="1436" alt="スクリーンショット 2017-01-07 16.29.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/71e9d08c-c80c-89e7-3098-48d68a66a087.png">

* 下記のようになっていれば作成完了
<img width="1436" alt="スクリーンショット 2017-01-07 16.31.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/4a9ef9b2-c16b-cb91-ee8e-5882b1e9f2e7.png">

### ポリシーの作成
* Thingに対して、AWS IoTの各種操作を許可するためのポリシーを作成
<img width="1436" alt="スクリーンショット 2017-01-08 0.34.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/0bb705ff-eb39-3060-81e7-ebc161790a8f.png">

* ポリシーの名前を設定し、Actionを「iot:*」、Resourceを「*」、Allowにチェックボックスを入れる。これで、iotのすべての操作に対して許可をする設定になります
<img width="1413" alt="スクリーンショット 2017-01-08 0.35.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/aa9d8660-fe27-42ff-86b9-4754a01b9efe.png">

### 証明書の作成
* 上記完了画面の「Security」から「Create certificate」をクリック
<img width="1435" alt="スクリーンショット 2017-01-07 16.44.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/9d90209d-d7aa-8ee5-8e7f-a1b0dbc84ce3.png">

* 完了すると下記の画面になるので、3つの証明書(Downloadリンク)をダウンロードし、「Activate」ボタンをクリック
<img width="1433" alt="スクリーンショット 2017-01-07 16.46.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/1e957760-5731-111b-1024-93397e11d767.png">

### 証明書にアタッチ
* 証明書にPolicyをアタッチ
<img width="1428" alt="スクリーンショット 2017-01-08 0.36.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/0c072aef-560a-c206-53e4-12d032e99a5d.png">
<img width="1434" alt="スクリーンショット 2017-01-08 0.36.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/adfcbddd-5e3f-a935-c952-704cf654e1c6.png">

* 証明書にThingをアタッチ
<img width="1429" alt="スクリーンショット 2017-01-08 0.37.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/0148be47-8cd1-31bc-0531-a049c1a675e6.png">
<img width="1431" alt="スクリーンショット 2017-01-08 0.37.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/d31c2580-0037-9c74-616f-2d25a3a308ca.png">

### Lambdaの準備①
AWS IoTのRuleに追加するためのLambdaファンクションを用意する。
本来は下記のいずれかでよかったのですが、DynamoDBへの書き込みが意図通りにいかなそうだったので改めて用意。

* [myThingsとAmazon Dash ButtonをAWS経由で繋げてみる](http://qiita.com/yumatsud/items/0771217c9c3efe44594b)で用意したLambdaファンクションを利用
* AWS IoTのRuleで直接DynamoDBへ書き込み(MacAddressを追加する方法がわからなかったので一旦これはなし)

用意したLambdaファンクションは下記

```write_logs.js
'use strict';

//var uuid = require('node-uuid');
const qs = require("querystring");
var date = new Date();

var AWS = require("aws-sdk");
var dynamo = new AWS.DynamoDB.DocumentClient();

exports.handler = (event, context, callback) => {
    // パラメータチェック
    if (!event.macAddress) {
        context.fail('macAddress is not specified')
    }
    
    // uuidを生成
    //var uuid = uuid.v4();
    var uuid = createUuid();
    
    // 更新内容をセット
    var item = {
        "logId": uuid,
        "createdAt":  Math.floor(date.getTime() / 1000),
        "macAddress": event.macAddress
    };
    
    var param = {
        TableName: 'Button',
        Item: item
    }
    dynamo.put(param, function(err, data) {
        if (err) {
            context.fail(err);
        } else {
            context.succeed(item)
        }
    });
};

/**
 * UUID(ランダム文字列)の生成
 * @return string UUID
 */
function createUuid() {
    var S4 = function() {
        return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
    }   
    return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4() +S4());
}
```

### Ruleの作成
* Rulesから「Create a rule」をクリック
<img width="1436" alt="スクリーンショット 2017-01-07 17.02.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/042ca1c2-b929-4a80-ea72-4b5067bc10f2.png">

* NameとDescriptionを入力
<img width="1436" alt="スクリーンショット 2017-01-07 17.05.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/95f5131b-3b83-1d25-9ad9-0cd9e48c7eeb.png">

* Message Sourceの設定
<img width="1432" alt="スクリーンショット 2017-01-07 17.09.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/e72d2ba1-d291-eeab-cab1-63420ff7702d.png">
各項目の説明は下記の「参照メモ」をご覧ください。

* 「Add action」で下記の画面に遷移し、Lambdaを選択
<img width="1436" alt="スクリーンショット 2017-01-07 17.27.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/6ff7e9d4-1ad3-28ca-ba69-dd239ce64d3f.png">

* 先ほど作成したLambdaファンクションを選択
<img width="1428" alt="スクリーンショット 2017-01-07 17.33.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/ac60da64-a568-33ae-6c52-4f5052abded7.png">

* 下記のようになっていれば「Create rule」をクリック
<img width="1434" alt="スクリーンショット 2017-01-07 17.34.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/d72fa2e9-fc4f-56bc-6143-dd1df9d2bd71.png">

* これでRuleの完成
<img width="1435" alt="スクリーンショット 2017-01-07 17.34.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/f3d80759-b297-c556-4a07-5f02d2d9dac3.png">

### 動作確認
* 下記コマンドを実行(Macでやってみましたが、その場合は[brew](http://brew.sh/index_ja.html)と[mosquitto](https://mosquitto.org/download/)のインストールが必要)

```
$ /usr/local/opt/mosquitto/bin/mosquitto_pub --cafile Downloads/rootCA.pem \
> --cert <<証明書>>.crt \
> --key <<プライベートキー>>.key \
> -h <<エンドポイント>>.iot.us-west-2.amazonaws.com \
> -p 8883 -d -t topic/button \
> -m '{"macAddress": "XX:XX:XX:XX:XX:XX"}'
Client mosqpub/16956-YukinoMac sending CONNECT
Client mosqpub/16956-YukinoMac received CONNACK
Client mosqpub/16956-YukinoMac sending PUBLISH (d0, q0, r0, m1, 'topic/button', ... (35 bytes))
Client mosqpub/16956-YukinoMac sending DISCONNECT
```

* DynamoDBに保存されていれば成功！！

## SORACOM Beamの準備
SORACOM Beamの設定はSORACOMのユーザーコンソールのグループ設定から行います。

### グループ設定
* メニューのグループからグループを追加
<img width="1428" alt="スクリーンショット 2017-01-07 21.48.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/e5da720c-2f13-0cf2-3def-d5f7e6b20906.png">

* MQTTエントリポイントを設定。下記のように設定したら、証明書をONにする。
<img width="1407" alt="スクリーンショット 2017-01-07 21.54.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/1a8a858c-cd16-53c2-9bf4-b26729971923.png">
<img width="587" alt="スクリーンショット 2017-01-07 22.03.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/a606c853-c922-0436-a638-a6d899a99ccb.png">

* 証明書の設定。上記でダウンロードした3つの証明書を下記のように入力。
<img width="600" alt="スクリーンショット 2017-01-07 22.08.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/2a0fcb1d-1f9c-310a-f700-9720058f22dc.png">

* 上記で設定したグループに、ラズパイに接続しているSIMを追加
<img width="890" alt="スクリーンショット 2017-01-07 22.18.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/1c275d18-43df-5248-e55b-8479f7bd43b3.png">

### ラズパイとの接続確認
* MQTTのクライアントの**Mosquitto**をインストール

```
$ sudo apt-get install -y mosquitto-clients
パッケージリストを読み込んでいます... 完了
・・・・・
mosquitto-clients (1.3.4-2) を設定しています ...
libc-bin (2.19-18+deb8u6) のトリガを処理しています ...
```
* インストールしたMosquittoクライアントを使って、AWS IoTにリクエストを投げてみる

```
$ mosquitto_pub -d -h beam.soracom.io -t topic/button -m '{"macAddress": "XX:XX:XX:XX:XX:XX"}' -p 1883
Client mosqpub/4091-darmaso-ra sending CONNECT
Client mosqpub/4091-darmaso-ra received CONNACK
Client mosqpub/4091-darmaso-ra sending PUBLISH (d0, q0, r0, m1, 'topic/button', ... (35 bytes))
Client mosqpub/4091-darmaso-ra sending DISCONNECT
```

* DynamoDBに保存されていれば成功！！

## AWS Lambdaの準備②
DynamoDBに保存されたら、myThings DevelopersのAPIへリクエストする部分を作成する。

* Lambdaのファンクション生成画面で「dynamodb-process-stream」を選択
<img width="1432" alt="スクリーンショット 2017-01-08 18.02.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/61ef1562-154f-f764-8581-371928f313c9.png">

* DynamoDB tableは準備済みのテーブル、Starting PositionはLateast、Enable Triggerにチェックを入れて次へ
<img width="1432" alt="スクリーンショット 2017-01-08 18.02.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/c8ae39c0-0a92-bf62-5af3-6402f7f5c6a9.png">


* コードはNode.js 4.3を選択。ソールコードは下記。

```request_mythings.js
'use strict';

const qs = require("querystring");
var https = require("https");
var date = new Date();
var dateString = createPostTimeString();

// myThings Developersに必要なリクエスト項目
var appid = "<<自分のAppID>>";
var secret = "<<自分のsecret>>";
var accessToken = "<<自分のアクセストークン>>";
var refreshToken = "<<自分のリフレッシュトークン>>";

exports.handler = (event, context, callback) => {
    var logId, createdAt, macAddress;
    event.Records.forEach((record) => {
        console.log('DynamoDB Record: %j', record.dynamodb);
        logId = record.dynamodb.NewImage.logId.S;
        console.log('logId: %j', record.dynamodb.NewImage.logId.S);
        createdAt = record.dynamodb.NewImage.createdAt.N;
        console.log('createdAt: %j', record.dynamodb.NewImage.createdAt.N);
        macAddress = record.dynamodb.NewImage.macAddress.S;
        console.log('macAddress: %j', record.dynamodb.NewImage.macAddress.S);
    });
    // 更新データチェック
    if (!logId || !createdAt || !macAddress) {
        context.fail('Updated data is invalid')
    }
    // myThings Developersへリクエスト
    requestDevelopers(context);
};

/**
 * "2016/12/21(水)"の形式の文字列を返す
 * @return string 日付文字列
 */
function createPostTimeString() {
    // 年
    var year = date.getFullYear();
    // 月
    var month = date.getMonth() + 1;
    if (month < 10) {
        month = '0' + month;
    }
    // 日
    var day = date.getDate();
    if (day < 10) {
        day = '0' + day;
    }
    // 曜日
    var weekDayList = [ "日", "月", "火", "水", "木", "金", "土" ];
    var weekDay = weekDayList[ date.getDay() ];
    
    return year+"/"+month+"/"+day+"("+weekDay+")";
}

/**
 * myThings Developersへのリクエスト
 * @return void
 */
function requestDevelopers(context) {
    // リクエストパラメータの生成
    var postArgs = {
        date: dateString
    }
    var postData = qs.stringify({
        "entry": JSON.stringify(postArgs),
    });
    
    // リクエスト設定
    var options = {
        hostname: "mythings-developers.yahooapis.jp",
        path: "/v2/services/<<個人のパス>>/mythings/<<個人のパス>>/run",
        port: 443,
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
            "Authorization": "Bearer " + accessToken,
        },
    };
    
    // リクエスト
    var req = https.request(options, function(res){
        // 401のとき
        if (res.statusCode == 401) {
            // コールバック付きのrefreshAccessTokenを呼ぶ
            refreshAccessToken(context);
            return;
        }

        // レスポンス処理
        res.on("data", function(body){
            var parseData = JSON.parse(body);
            if(typeof( parseData["flag"] ) != "undefined") {
                context.succeed("カスタムトリガーの実行リクエストを受け付けました。")
            } else {
                console.log("カスタムトリガーの実行リクエストの受付に失敗しました。:"+body);
            }
        });
    })
    .on("error", function(res){
        context.fail("カスタムトリガーの実行リクエストの受付に失敗しました。:"+res.content);
    });
    req.end(postData)
}

/**
 * アクセストークンのリフレッシュ
 */
function refreshAccessToken(context) {
    console.log("refreshAccessTokenにきたよ");
    // リフレッシュ用データのセット
    var reqData = qs.stringify({
        "grant_type": "refresh_token",
        "refresh_token": refreshToken
    });
    // リクエスト設定
    var buffer = new Buffer(appid + ":" + secret, "ascii");
    var options = {
        hostname: "auth.login.yahoo.co.jp",
        path: "/yconnect/v1/token",
        port: 443,
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
            "Authorization": "Basic " + buffer.toString("base64"),
        }
    };

    // リクエスト実行
    var req = https.request(options, function(res) {
        console.log("refreshAccessTokenのrequestのなかにきたよ");
        // 401の場合
        if(res.statusCode == 401) {
            context.fail("リフレッシュトークンの有効期限が切れました。myThings Developersのサンプルコードからリフレッシュトークンを再取得して下さい。");
        } else if(res.statusCode != 200) {
            context.fail("カスタムトリガーの実行リクエストの受付に失敗しました。:"+res.content);
        }

        // レスポンス処理
        res.on('data', function(body){
            var parseData = JSON.parse(body);
            accessToken = parseData['access_token'];
            requestDevelopers(context);
        }); 
    });

    // POSTデータのリクエスト
    req.end(reqData);
}
```

* ラズパイから再度下記のコマンドを打ち、Slackに投稿&メールが届いて入ればOK!!

```
$ mosquitto_pub -d -h beam.soracom.io -t topic/button -m '{"macAddress": "XX:XX:XX:XX:XX:XX"}' -p 1883
Client mosqpub/4091-darmaso-ra sending CONNECT
Client mosqpub/4091-darmaso-ra received CONNACK
Client mosqpub/4091-darmaso-ra sending PUBLISH (d0, q0, r0, m1, 'topic/button', ... (35 bytes))
Client mosqpub/4091-darmaso-ra sending DISCONNECT
```
<img width="1212" alt="スクリーンショット 2017-01-08 18.35.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/71637/0fd11596-2c70-5319-2d3f-155032ce3cc5.png">


## ラズパイにスイッチをつける
* 下記のように配線 (スイッチを押している間のみLEDも光るようにしてみてあります)
![IMG_9229.JPG](https://qiita-image-store.s3.amazonaws.com/0/71637/5cde3cc0-e52d-c5de-e036-c25dc78388eb.jpeg)
※詳しくは、[タクトスイッチを使ってLEDを点灯してみる](http://iwasakiyouhei.com/post-2278/)をご参照下さいmm

* PahoのPythonクライアントであるpaho-mqttをインストール

```
$ sudo pip install paho-mqtt
Downloading/unpacking paho-mqtt
  Downloading paho-mqtt-1.2.tar.gz (49kB): 49kB downloaded
  Running setup.py (path:/tmp/pip-build-wI_jbk/paho-mqtt/setup.py) egg_info for package paho-mqtt
    
Installing collected packages: paho-mqtt
  Running setup.py install for paho-mqtt
    
Successfully installed paho-mqtt
Cleaning up...
```

* ボタンの制御部分のソースコードは下記

```button.py
#! /usr/bin/env python

import RPi.GPIO as GPIO
import time
import json
import paho.mqtt.client as mqtt

def on_connect(client, userdata, rc):
  print("Connected with result code " + str(rc))

def on_disconnect(client, userdata, rc):
  if rc != 0:
    print("Unexpected disconnection.")

def on_publish(client, userdata, mid):
  print("publish: {0}".format(mid))

def main():
  # Setting GPIO
  GPIO.cleanup()
  GPIO.setmode(GPIO.BCM)
  GPIO.setup(24, GPIO.IN)
  GPIO.setup(25, GPIO.OUT)

  # Setting MQTT
  client = mqtt.Client()
  client.on_connect = on_connect
  client.on_disconnect = on_disconnect
  client.on_publish = on_publish
  client.connect("beam.soracom.io", 1883, 60)

  try:
    while True:
      if ( GPIO.input(24) == GPIO.HIGH ):
        GPIO.output(25, GPIO.HIGH)
        message = json.dumps({"macAddress":"XX:XX:XX:XX:XX:XX"})
        client.publish("topic/button", message)
      else:
        GPIO.output(25, GPIO.LOW)
      time.sleep(0.5)
  except KeyboardInterrupt:
    GPIO.cleanup()

if __name__ == '__main__':
  main()
```

* 下記のようにコマンドを実行し、タクトスイッチを押して、「publish: 1」と表示されていればOK

```
$ sudo python button.py 
publish: 1
```

# 完成デモ
![IMG_4147.GIF](https://qiita-image-store.s3.amazonaws.com/0/71637/13d66e9c-7ddb-30a1-3ff7-80ade77fa3f2.gif)

なんとか完成しました！！

# 最後に
ラズパイの準備から色々と大変でしたが、なんとか完成までこぎつけることができました。
ラズパイをいじるのはそれなりに知識がないと大変でしたが、
AWS側はサーバを用意しなくてもGUI上でポチポチしてコードがUploadするだけで十分なのは良いですね！！
ラズパイもdockerとかでイメージを管理してしまえば、構築に手間もかからないので、
今後はサーバレスアーキテクトとコンテナ管理をうまくやっていけばある程度のものはすぐにできてしまいそうな予感。
次はdockerでのラズパイ管理もチャレンジしてみたいと思いますmm

# 参照メモ
* Rule設定時の「Message Source」の項目メモ

| 項目 | 説明 | 備考 |
|:--|:--|:--|
| Using SQL version | SQLバージョンの選択。選択しなくてもOK。 | デフォルト値がある模様。 |
| Attribute | 対象とするデータ。 | 今回はすべてのデータなので「*」を利用。 |
| Topic filter | 対象とするトピック。 | 今回は「topic/button」を指定する。全てのTopicに送信されたデータは「#」を指定。　|

* [mosquitto_pub](https://mosquitto.org/man/mosquitto_pub-1.html)
* [paho-mqtt](https://pypi.python.org/pypi/paho-mqtt)
* [PPP接続とLAN接続のデフォルトゲートウェイを切り替えPPP接続を優先させる](http://qiita.com/dietposter/items/ac9108b8eaebb90e8050)

# 参考記事
* [SORACOM Beam を使用して AWS IoT と接続する](https://dev.soracom.io/jp/docs/aws_iot_guide_console/)
* [AWS IoTとRaspberry PiではじめるIoT超入門](http://codezine.jp/article/detail/9530)
* [タクトスイッチを使ってLEDを点灯してみる](http://iwasakiyouhei.com/post-2278/)
* [Raspberry PiのPythonでDS18B20のセンシングデータをPahoのMQTTからpublishする](http://qiita.com/masato/items/6b73fc01ec5d488f8d42)