こんにちは!IoTLT Advent Calendar 2018 12日目担当のchinoppyです!
真ん中の日に、日本の真ん中の長野から再び?!お送りします。
会社の出社/退社状態を管理しているWebページ(以下、ホワイトボードと記載)をIoTを使ってワンタッチで状態(出勤/退勤)を変更できるようにしてみました。
これで、1秒でも早く仕事を始めて、1秒でも早く帰宅できるようになりましたw
きっかけ
- もともとは、物理ホワイトボードだったものを、社員の有志( @moonwalkerpoday )が電子化
- これによりslackから状態を変更可能にしたりと簡素化する動きがでてきた
- そんななか、社内のガジェットをあさっていたら・・・
社内にあったガジェット等
ソニー SONY 非接触ICカードリーダー/ライター PaSoRi
Raspberry Pi 3 Model B + Raspberry Pi Sense HAT 付き
ちなみに、Sense HatはAstro Piプロジェクトというのために開発されたものなのですね。
なんと、宇宙空間で使うことを想定したものだそうです!夢を感じる。
TP-Link WiFiスマートプラグ
・・・・・・あっ、会社の入り口はNFCカードで、ひとりひとりにカードが支給されている!
ということで、作成開始
ちなみに
- エラー処理などは適当なところがあるのでご了承を。。。
- ぶっちゃけRaspberry Piから直接、ホワイトボードのWeb APIたたけるのですが・・・そこは考えないでくださいw
- (AWS IoT使ったりしてそのあたりの勉強とか、知見も得たかったので。。。)
構成
- タッチすると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系のみの対応みたいですね。。。
ソースはこんな感じ。
# -*- 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の制御を行う
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を実行し、ホワイトボードの変更などを行う
こんなかたちで、ルールを登録
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)
slack通知例:
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 例:
# 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)にシャットダウン指示
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例:
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トピックを使用して確認
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は平日のみ
にするように設定
これで、すべての実装、設定が完了!と思ったのですが、Systemdで自動起動するようにしておきます
#!/bin/bash
source /home/pi/touch_attendance/env2/bin/activate
cd /home/pi/touch_attendance
sudo python /home/pi/touch_attendance/main2.py
[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
#!/bin/bash
source /home/pi/touch_attendance/env3/bin/activate
cd /home/pi/touch_attendance
python /home/pi/touch_attendance/main3.py
[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と、変更後の状態が表示される
【出社だよー!】
- slackに通知される
【退社だよー!】
- slackに通知される
Shadowの状態がUpdateされたときにslackへ通知
Lambdaからの起動指示/シャットダウン指示がslackへ通知される
Raspberry PiがUpdateしたときにslackへ通知
「・・・起動します」〜「・・・シャットダウンします」までは、ホワイトボードの変更が可能
これでRaspberry Piの状態(起動中or停止中)がわかる!
想定外だったこと
IDmがタッチするたびにかわった?!
後輩ちゃん:「chinoppyさん、スマフォでもできるんですよね?」
chinoppy:「オフコース!」
後輩ちゃんがPaSoRiにタッチしたので、
DynamoDBのIDmとユーザ情報を保持しているテーブルにユーザ情報を登録
chinoppy:「これでできるよー」
後輩ちゃん:「最高ですね!」
もう一度、後輩ちゃんがタッチ
chinoppy:・・・「あれ?ホワイトボードかわらないなぁ・・・」
DynamoDBのIDmとユーザ情報を保持しているテーブルをみると
また別のIDmが登録されている・・・
もう一度、後輩ちゃんにタッチしてもらうと、また別のIDmが登録された・・・
今後の課題となりました・・・orz
最後に
今後は、勤怠もWeb化してあるので、タッチで時間がはいるようにしたい!
それでは、シャットダウンしますー
明日は、_rio_さんです!