Posted at

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

More than 1 year has passed since last update.


概要

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

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


やること


  1. ラズパイにスイッチをつける

  2. ラズパイのスイッチを押すと、SORACOM経由でAWSへ

  3. SORACOM経由での通信はMQTTで、MQTTSにする部分はSORACOM Beamで吸収

  4. AWSへの部分はAWS IoTで実現

  5. AWS IoTからはDynamoDBへデータを保存

  6. AWS DynamoDBにデータが保存されたら、AWS Lambdaをキック

  7. AWS LambdaからmyThings Developers経由で勤怠連絡

  8. 勤怠連絡にはSlack、Y!メールを利用


用意するもの


  • Raspberry Pi3

  • 3G USB ドングル (FS01BU)

  • SORACOM Air

  • AWS IoT

  • AWS DynamoDB

  • AWS Lambda

  • myThings Developers


事前準備


やったこと

上記のやることの1〜6までの作業をやっていく。

順番は若干違いますが、一通りやってみました。


AWS IoTの準備

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


Thingの作成


  • AWS IoTコンソールの「Registry」から「Register thing」をクリック

    スクリーンショット 2017-01-07 16.27.39.png


  • Thing名を入力して「Create thing」をクリック

    スクリーンショット 2017-01-07 16.29.43.png


  • 下記のようになっていれば作成完了

    スクリーンショット 2017-01-07 16.31.16.png



ポリシーの作成


  • Thingに対して、AWS IoTの各種操作を許可するためのポリシーを作成

    スクリーンショット 2017-01-08 0.34.10.png


  • ポリシーの名前を設定し、Actionを「iot:」、Resourceを「」、Allowにチェックボックスを入れる。これで、iotのすべての操作に対して許可をする設定になります

    スクリーンショット 2017-01-08 0.35.06.png



証明書の作成


  • 上記完了画面の「Security」から「Create certificate」をクリック

    スクリーンショット 2017-01-07 16.44.13.png


  • 完了すると下記の画面になるので、3つの証明書(Downloadリンク)をダウンロードし、「Activate」ボタンをクリック

    スクリーンショット 2017-01-07 16.46.15.png



証明書にアタッチ


  • 証明書にPolicyをアタッチ

    スクリーンショット 2017-01-08 0.36.45.png

    スクリーンショット 2017-01-08 0.36.52.png


  • 証明書にThingをアタッチ

    スクリーンショット 2017-01-08 0.37.17.png

    スクリーンショット 2017-01-08 0.37.23.png



Lambdaの準備①

AWS IoTのRuleに追加するためのLambdaファンクションを用意する。

本来は下記のいずれかでよかったのですが、DynamoDBへの書き込みが意図通りにいかなそうだったので改めて用意。

用意した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」をクリック

    スクリーンショット 2017-01-07 17.02.07.png


  • NameとDescriptionを入力

    スクリーンショット 2017-01-07 17.05.45.png


  • Message Sourceの設定

    スクリーンショット 2017-01-07 17.09.05.png

    各項目の説明は下記の「参照メモ」をご覧ください。


  • 「Add action」で下記の画面に遷移し、Lambdaを選択

    スクリーンショット 2017-01-07 17.27.20.png


  • 先ほど作成したLambdaファンクションを選択

    スクリーンショット 2017-01-07 17.33.47.png


  • 下記のようになっていれば「Create rule」をクリック

    スクリーンショット 2017-01-07 17.34.01.png


  • これでRuleの完成

    スクリーンショット 2017-01-07 17.34.17.png



動作確認


  • 下記コマンドを実行(Macでやってみましたが、その場合はbrewmosquittoのインストールが必要)

$ /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のユーザーコンソールのグループ設定から行います。


グループ設定


  • メニューのグループからグループを追加

    スクリーンショット 2017-01-07 21.48.44.png


  • MQTTエントリポイントを設定。下記のように設定したら、証明書をONにする。

    スクリーンショット 2017-01-07 21.54.39.png

    スクリーンショット 2017-01-07 22.03.49.png


  • 証明書の設定。上記でダウンロードした3つの証明書を下記のように入力。

    スクリーンショット 2017-01-07 22.08.51.png


  • 上記で設定したグループに、ラズパイに接続しているSIMを追加

    スクリーンショット 2017-01-07 22.18.15.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」を選択

    スクリーンショット 2017-01-08 18.02.12.png


  • DynamoDB tableは準備済みのテーブル、Starting PositionはLateast、Enable Triggerにチェックを入れて次へ

    スクリーンショット 2017-01-08 18.02.25.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

スクリーンショット 2017-01-08 18.35.13.png


ラズパイにスイッチをつける

$ 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

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


最後に

ラズパイの準備から色々と大変でしたが、なんとか完成までこぎつけることができました。

ラズパイをいじるのはそれなりに知識がないと大変でしたが、

AWS側はサーバを用意しなくてもGUI上でポチポチしてコードがUploadするだけで十分なのは良いですね!!

ラズパイもdockerとかでイメージを管理してしまえば、構築に手間もかからないので、

今後はサーバレスアーキテクトとコンテナ管理をうまくやっていけばある程度のものはすぐにできてしまいそうな予感。

次はdockerでのラズパイ管理もチャレンジしてみたいと思いますmm


参照メモ


  • Rule設定時の「Message Source」の項目メモ

項目
説明
備考

Using SQL version
SQLバージョンの選択。選択しなくてもOK。
デフォルト値がある模様。

Attribute
対象とするデータ。
今回はすべてのデータなので「*」を利用。

Topic filter
対象とするトピック。
今回は「topic/button」を指定する。全てのTopicに送信されたデータは「#」を指定。 


参考記事