22
15

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.

IoTLTAdvent Calendar 2018

Day 12

IoTで出社/退社をワンタッチ化

Last updated at Posted at 2018-12-11

こんにちは!IoTLT Advent Calendar 2018 12日目担当のchinoppyです!

真ん中の日に、日本の真ん中の長野から再び?!お送りします。

会社の出社/退社状態を管理しているWebページ(以下、ホワイトボードと記載)をIoTを使ってワンタッチで状態(出勤/退勤)を変更できるようにしてみました。
これで、1秒でも早く仕事を始めて、1秒でも早く帰宅できるようになりましたw

【ホワイトボード】
image.png

きっかけ

  • もともとは、物理ホワイトボードだったものを、社員の有志( @moonwalkerpoday )が電子化
  • これによりslackから状態を変更可能にしたりと簡素化する動きがでてきた
  • そんななか、社内のガジェットをあさっていたら・・・

社内にあったガジェット等

ソニー SONY 非接触ICカードリーダー/ライター PaSoRi

image.png

Raspberry Pi 3 Model B + Raspberry Pi Sense HAT 付き

image.png

ちなみに、Sense HatはAstro Piプロジェクトというのために開発されたものなのですね。
なんと、宇宙空間で使うことを想定したものだそうです!夢を感じる。

TP-Link WiFiスマートプラグ

image.png

・・・・・・あっ、会社の入り口はNFCカードで、ひとりひとりにカードが支給されている!
ということで、作成開始

ちなみに

  • エラー処理などは適当なところがあるのでご了承を。。。
  • ぶっちゃけRaspberry Piから直接、ホワイトボードのWeb APIたたけるのですが・・・そこは考えないでくださいw
  • (AWS IoT使ったりしてそのあたりの勉強とか、知見も得たかったので。。。)

構成

スクリーンショット 2018-12-10 10.28.25.png
  • タッチするとAWS IoT経由でLambdaを実行しホワイトボード更新、slackへ状態を通知
  • CloudWatchのルールのスケジュール式を使用して、Lambdaを実行して、Shadow経由でRaspberry Piの状態(起動/シャットダウンなど)を制御、結果をslackへ通知
  • ShadowのUpdate結果をslackへ通知
  • TP-Link WiFiスマートプラグ経由でRaspberry Piに電源供給し、スマートプラグのスケジュール機能を使用して電源ON/OFFを行う

実装

タッチ部分

  • Python2系を使用
  • ライブラリはnfcpyを使用
  • 主な処理
    • タッチしたときのIDmを取得、DBへ保存
    • 取得したIDmをSense HATのLEDに表示

nfcpyの情報はたくさんあり、すごく参考になりました!ありがとうございます。
SenseHATもPythonライブラリが提供されていて、超簡単にLEDにメッセージが表示できる!
でも、Python2系のみの対応みたいですね。。。
ソースはこんな感じ。

main2.py
# -*- coding: utf-8 -*-

import nfc
from sense_hat import SenseHat

from models import SqliteDbManager, TouchHistoryModel


class MyCardReader(object):

    idm = None

    def on_connect(self, tag):
        print("touched")
        print(tag)
        self.idm = tag.identifier.encode("hex").upper()
        print(self.idm)
        return True

    def read_id(self):
        clf = nfc.ContactlessFrontend('usb')
        try:
            clf.connect(rdwr={'on-connect': self.on_connect})
        finally:
            clf.close()


def main():

    sense = SenseHat()
    cr = MyCardReader()

    while True:
        print("touch card:")

        cr.read_id()
        print("released")
        print(cr.idm)

        db_manager = SqliteDbManager()
        with db_manager.transaction() as session_pq:

            entry = TouchHistoryModel(idm=cr.idm)
            session_pq.add(entry)

        sense.clear()
        sense.show_message(cr.idm, text_colour=[0, 0, 100])

        cr.idm = None


if __name__ == '__main__':

    main()

そして、IDmをテーブルに保存するようにしました

なぜ保存するようにしたか

  • 基本的にはPython3で実装したい
  • でもnfcpyは2系のみ
  • それなら、タッチ部分は「2系」、それ以外は「3系」で実装しよう
  • DBでやりとりしよう!

ということにしましたw
DBはSqlite、ORMはSQLAlchemyを使用

送信部分(タッチ部分以外)

  • Python3系を使用
  • AWS IoTへの送信は「AWS IoT Device SDK for Python」を使用
  • 主な処理
    • ShadowのGet、Update処理
    • タッチ情報をDBから取得、AWS IoTへPublish
    • Subscribeして勤怠の変更後の状態を取得、表示
    • Shadowの値により、Raspberry Piの制御を行う
main3.py
def main(loop):

    sense = SenseHat()

    touch_attendance_shadow = TouchAttendanceShadowData('{}')
    raspberry_pi_shadow = RaspberryPiShadowData('{}')

    count_sec = 0
    while True:

        # AWS IoT Shadow Get, Update
        if is_get_shadow(count_sec):
            # Shadowとの通信開始
            comm_aws_iot_shadow = CommAwsIoTShadow(thing_name)
            result = comm_aws_iot_shadow.connect()
            comm_aws_iot_shadow.create_shadow_handler_with_name(thing_name)

            # Shadowを取得
            shadow_payload = shadow_get(comm_aws_iot_shadow, loop)

            touch_attendance_shadow = TouchAttendanceShadowData(shadow_payload)
            raspberry_pi_shadow = RaspberryPiShadowData(shadow_payload)

            # Shadowを更新(deltaがある場合のみ)
            shadow_buf = BaseShadowData(shadow_payload)
            update_payload = shadow_buf.get_update_payload()
            shadow_update(comm_aws_iot_shadow, update_payload, loop)

            comm_aws_iot_shadow.disconnect()

        db_manager = SqliteDbManager()
        with db_manager.transaction() as session_pq:

            if touch_attendance_shadow.status:
                # AWS IoTへ送信
                touch_history_model = TouchHistoryModel()
                touch_history_entries = touch_history_model.select_all(session_pq)
                ok_ids = sub_pub_touch_histories(touch_history_entries, sense)
                touch_history_model.delete_by_id_list(session_pq, ok_ids)

        if raspberry_pi_shadow.is_reboot(count_sec):
            reboot(sense)

        if raspberry_pi_shadow.is_shutdown():
            shutdown(sense)

        time.sleep(1)
        count_sec += 1

ShadowのGet、Update処理

定期的に、ShadowのGetを行い、情報の取得とdeltaが存在する場合は、Updateを行う(5分間隔)

タッチ情報をDBから取得、AWS IoTへPublish、Subscribe

  • 定期的にDBをselectして、タッチした情報が存在する場合、AWS IoTへPublish
  • Publishが成功した場合は、DBのレコードを削除

Subscribeして勤怠の変更後の状態を取得、表示

  • 更新後の状態をLambdaでpublishしているので、Raspberry Pi側でSubscribe
  • 変更後の状態をSense HATのLEDに表示
  • ちなみに失敗するときがあるので今後の課題・・・

Shadowの値により、Raspberry Piの制御を行う

設定値などは、Shadowにもたせて、Raspberry Pi側はその値をみて状態を制御するようにしました
Shadowの内容はこんな感じ

{
  "desired": {
    "raspberry_pi": {
      "status": false,
      "reboot_interval_hour": 6
    }
  },
  "reported": {
    "raspberry_pi": {
      "status": false,
      "reboot_interval_hour": 6
    }
  }
}
  • 起動後の累計時間が、再起動間隔(reboot_interval_hour)(単位:時)を超えた場合は再起動
    • 再起動処理をします表示をSense HATに表示
    • sudo shutdown -r now
  • statusが「false」になった場合は、Raspberry Piをシャットダウン
    • シャットダウンします表示をSense HATに表示
    • sudo shutdown -h now

AWS

  • serverless frameworkを使用して構築
  • 主な機能
    • Raspberry PiからPublishされたデータはAWS IoTのルールを使用してLambdaを実行し、ホワイトボードの変更などを行う
    • CloudWatchのルールのスケジュール式を使用して定期的にLambdaを実行しShadowをUpdate
    • ShadowのUpdate後にAWS IoTのルールを使用してLambdaを実行し結果をslackに通知

Raspberry PiからPublishされたデータはAWS IoTのルールを使用してLambdaを実行し、ホワイトボードの変更などを行う

こんなかたちで、ルールを登録

serverless.yml
functions:
  update_attendance:
    handler: handler.update_attendance
    role: LambdaAttendanceRole
    events:
      - iot:
          name: "UpdateAttendance"
          sql: "SELECT *, topic(2) AS name FROM 'touch/+/histories'"
          description: "update white borad"
  • Lambda内の処理(Python3)
    • IDmとユーザ情報を管理するテーブルからユーザ情報を取得
      • ない場合は、そのIDmの登録を行う
      • slackにその旨通知されるので、AWSコンソールから手動でユーザ情報を登録するw
    • ※以降はIDmに紐づくユーザ情報がある場合
    • 取得したユーザ情報をもとに、ホワイトボードから現在の状態を取得
      • Pythonのrequestsライブラリを使用
    • 取得した状態を反転しホワイトボードを更新
    • 更新した状態をslackへ通知
      • Pythonのslackwebライブラリを使用
    • 更新後の状態をPublish

slack通知例:

handler.py
import ssl
from urllib.error import URLError, HTTPError
import slackweb



    def notify([webhookのURL], message):

        ssl._create_default_https_context = ssl._create_unverified_context
        slack = slackweb.Slack(url=[webhookのURL])

        try:
            slack.notify(text=message, username="attendance-bot", icon_emoji=":chinoppy:")
        except HTTPError as e:
            [エラー処理]
        except URLError as e:
            [エラー処理]

Publish 例:

handler.py
# AWS IoTエンドポイント
endpoint = '[エンドポイント]'
# IoTDataPlaneクライアント
iot_data = boto3.client('iot-data', endpoint_url=endpoint)



    response = iot_data.publish(
        topic='touch/' + thing_name + '/histories/res',
        qos=1,
        payload=json.dumps({
            "更新後の状態": 更新後の状態出勤退社,
        })
    )

CloudWatchのルールのスケジュール式を使用して定期的にLambdaを実行しShadowをUpdate

今のところは、Raspberry Piを

  • 6時(JST)に起動指示
  • 22時(JSL)にシャットダウン指示
serverless.yml
functions:
  start_attendance:
    handler: handler.start_attendance
    description: start attendance
    role: LambdaAttendanceRole
    events:
      - schedule: cron(0 21 ? * * *)
  stop_attendance:
    handler: handler.stop_attendance
    description: stop attendance
    role: LambdaAttendanceRole
    events:
      - schedule: cron(0 13 ? * * *)

ShadowのUpdate例:

handler.py
def _shadow_update(thing_name, update_payload):

    # Thing Shadowの有無を取得
    is_shadow = _is_shadow(thing_name)
    # shadowが存在する場合、updateする
    if not is_shadow:
        return

    iot_data.update_thing_shadow(
        thingName=thing_name,
        payload=update_payload
    )

def _is_shadow(thing_name):
    try:
        response = iot_data.get_thing_shadow(
            thingName=thing_name
        )
        payload = response['payload']
    except Exception as e:
        print(e.args)
        return False

    return True

ShadowのUpdate後にAWS IoTのルールを使用してLambdaを実行し結果をslackに通知

ShadowがUpdateされたかを、予約されたMQTTトピックを使用して確認

serverless.yml
functions:
  notification_shadow_update_accepted:
    handler: handler.notification_shadow_update_accepted
    role: LambdaAttendanceRole
    events:
      - iot:
          name: "NotificationShadowUpdateAccepted"
          sql: "SELECT *, topic(3)  AS name FROM '$aws/things/+/shadow/update/accepted'"
          description: "shadow update accepted"
  notification_shadow_update_rejected:
    handler: handler.notification_shadow_update_rejected
    role: LambdaAttendanceRole
    events:
      - iot:
          name: "NotificationShadowUpdateRejected"
          sql: "SELECT *, topic(3) AS name FROM '$aws/things/+/shadow/update/rejected'"
          description: "shadow update rejected"
  notification_shadow_get_rejected:
    handler: handler.notification_shadow_get_rejected
    role: LambdaAttendanceRole
    events:
      - iot:
          name: "NotificationShadowGetRejected"
          sql: "SELECT *, topic(3) AS name FROM '$aws/things/+/shadow/get/rejected'"
          description: "shadow get rejected"

TP-Link WiFiスマートプラグ

  • Wifiに接続しておけばどこからでも、アプリで電源のON/OFFができる
  • スケジュール機能がある

Raspberry Pi側は

  • 6時に起動指示
  • 22時にシャットダウン指示

なので、TP-Link WiFiスマートプラグ側を

  • 7時に電源ON
  • 22時10分に電源OFF(Shadowの状態を5分間隔でチェックしているので余裕をもたせて)
  • 電源のON/OFFは平日のみ

にするように設定

IMG_2668.png

これで、すべての実装、設定が完了!と思ったのですが、Systemdで自動起動するようにしておきます

start2.sh
#!/bin/bash

source /home/pi/touch_attendance/env2/bin/activate
cd /home/pi/touch_attendance
sudo python /home/pi/touch_attendance/main2.py
touch_attendance2.service(タッチ部分)
[Unit]
Description=touch attendance Python2

[Service]
Type=simple
WorkingDirectory=/home/pi/touch_attendance/
ExecStart=/home/pi/touch_attendance/start2.sh
Restart=always

[Install]
WantedBy = multi-user.target
start3.sh
#!/bin/bash

source /home/pi/touch_attendance/env3/bin/activate
cd /home/pi/touch_attendance
python /home/pi/touch_attendance/main3.py
touch_attendance3.service(タッチ部分以外)
[Unit]
Description=touch attendance Python3

[Service]
Type=simple
WorkingDirectory=/home/pi/touch_attendance/
ExecStart=/home/pi/touch_attendance/start3.sh
Restart=always

[Install]
WantedBy = multi-user.target
# シンボリックリンクをはる
$ sudo ln -s /home/pi/touch_attendance/touch_attendance2.service /etc/systemd/system/touch_attendance2.service
$ sudo ln -s /home/pi/touch_attendance/touch_attendance3.service /etc/systemd/system/touch_attendance3.service
# 自動起動設定
$ sudo systemctl enable touch_attendance2
$ sudo systemctl enable touch_attendance3

準備完了!

さぁ試してみよう!

タッチ!

PaSoRiにカードをタッチするとLEDにIDmと、変更後の状態が表示される

【出社だよー!】

ezgif.com-video-to-gif (1).gif

  • slackに通知される
スクリーンショット_2018-12-10_1_10_09.png

【退社だよー!】

ezgif.com-video-to-gif (2).gif

  • slackに通知される
スクリーンショット_2018-12-10_1_10_23.png

Shadowの状態がUpdateされたときにslackへ通知

Lambdaからの起動指示/シャットダウン指示がslackへ通知される

時間も想定どおり!
スクリーンショット_2018-12-10_1_17_19.png

Raspberry PiがUpdateしたときにslackへ通知

「・・・起動します」〜「・・・シャットダウンします」までは、ホワイトボードの変更が可能
これでRaspberry Piの状態(起動中or停止中)がわかる!

スクリーンショット_2018-12-10_1_19_20.png スクリーンショット_2018-12-10_1_10_09.png ・・・ スクリーンショット_2018-12-10_1_10_23.png スクリーンショット_2018-12-10_1_19_37.png

想定外だったこと

IDmがタッチするたびにかわった?!

後輩ちゃん:「chinoppyさん、スマフォでもできるんですよね?」
chinoppy:「オフコース!」

後輩ちゃんがPaSoRiにタッチしたので、
DynamoDBのIDmとユーザ情報を保持しているテーブルにユーザ情報を登録

chinoppy:「これでできるよー」
後輩ちゃん:「最高ですね!」

もう一度、後輩ちゃんがタッチ

chinoppy:・・・「あれ?ホワイトボードかわらないなぁ・・・」

DynamoDBのIDmとユーザ情報を保持しているテーブルをみると
また別のIDmが登録されている・・・

もう一度、後輩ちゃんにタッチしてもらうと、また別のIDmが登録された・・・
今後の課題となりました・・・orz

最後に

今後は、勤怠もWeb化してあるので、タッチで時間がはいるようにしたい!
それでは、シャットダウンしますー

ezgif.com-video-to-gif.gif

明日は、_rio_さんです!

22
15
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
22
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?