5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

概要

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

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

やること

  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

事前準備

やったこと

上記のやることの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に送信されたデータは「#」を指定。 

参考記事

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?