この記事はRaspberry Pi Advent Calendar 2021 17日目の記事です。
作ったもの
研究室の入退室を報告するSlack Botを作りました。
iPadで入室/退室を選択してカードリーダに学生証をタッチすると、Slackのチャンネルでメンション共に入退室が報告されます。
@tos pic.twitter.com/j0f7gSlIKO
— さくあ@思いつきぶん投げ(?) (@sakua_create) December 16, 2021
新型コロナの影響で、研究室の入室/退室の管理が必要になりました。
今までは、「Slackで手打ちで入室を報告→退室時に✅ リアクションをつける」という運用をしていましたが、これが結構面倒だったというのが作った動機です。
材料
- Raspberry Pi 4
- SONY PaSoRi RC-320
- iPad
- Slackアカウント
システム
全体構成
流れ
学生証登録済みの場合
- iPadで入室か退室かを選択
- 学生証をカードリーダにタッチ
- Slackの入退室チャンネルでメンションと共に報告される
学生証未登録の場合
- 未登録の学生証をカードリーダにタッチ
- 作成者(私)にSlackBotからIDm(識別番号的なもの)が届く
- 作成者がタッチした人の
Slackのアカウントと学生証を結びつけたものをラズパイ内の環境変数に追加する - 登録完了!
ファイル構成
slackbot
├ .env (IDmとSlackのmember ID紐付け用環境変数)
├ libpafe (PaSoRi RC-320用のライブラリ)
├ server.py (Flaskサーバ)
├ idm_slack.py (IDm読み取り&Slack投稿)
├ state.txt (入退室状態保持ファイル)
└ templates
└ index.html (入退室切替用Webページ)
実装
IDmとSlackのmember ID紐付け用環境変数
SlackのToken, Slackのmember ID, そして登録する学生証のIDmと登録者のSlackアカウントのmember IDのペアを環境変数に入れておきます。
どれも次の「IDm読み取り&Slackに投稿するプログラム」で使用します。
TOKEN="SlackのToken"
ADMIN_ID="自分のSlackのmember ID"
"登録者の学生証のIDm"=<@"登録者のSlackのmember ID">
・
・
・
IDm読み取り&Slackに投稿するプログラム
PaSoRiからIDmを読み取って、Slackに投稿するプログラムです。
環境
- Python 2.7
- libpafe (PaSoRi用ライブラリ)
- libusb-dev (libpafeの前提ライブラリ)
- python-dotenv (環境変数読み込み用ライブラリ)
- slacker (Slack投稿用ライブラリ)
研究室にあったICカードリーダPaSoRi RC-320は少し古いため、主流だと思われるnfcpyを使用できず、代わりにlibpafeを使用しました。libpafeはPythonの2系でしか動かないため、2系を使っています。
後述する「Flaskサーバ」は3系で動いているため、ラズパイ内には2種類のバージョンのPythonが共存していることになります。
$ sudo apt-get install libusb-dev
$ git clone https://github.com/rfujita/libpafe.git
$ cd libpafe
$ ./configure
$ make
$ sudo make install
$ pip install python-dotenv
$ pip install slacker
プログラム
プログラム作成にあたって、以下の記事を参考にさせて頂きました。
# coding: utf-8
from dotenv import load_dotenv
load_dotenv()
import os
import time
from slacker import Slacker
# 環境変数からSlackのトークン取得
TOKEN = os.getenv('TOKEN')
slack = Slacker(TOKEN)
FELICA_POLLING_ANY = 0xffff
ADMIN_ID = os.getenv('ADMIN_ID')
state = 'enter'
state_statement = {
'enter': '入室',
'leave': '退室'
}
if __name__ == '__main__':
# PaSoRiの初期設定
libpafe = cdll.LoadLibrary("/usr/local/lib/libpafe.so")
libpafe.pasori_open.restype = c_void_p
pasori = libpafe.pasori_open()
libpafe.pasori_init(pasori)
try:
while True:
# ループでPaSoRiからのIDmを読み取る
libpafe.felica_polling.restype = c_void_p
felica = libpafe.felica_polling(pasori, FELICA_POLLING_ANY, 0, 0)
idm = c_ulonglong()
libpafe.felica_get_idm.restype = c_void_p
libpafe.felica_get_idm(felica, byref(idm))
# フォーマット
idm_16 = "%016X" % idm.value
# 読み取れてなかった場合ループの頭へ
print(idm_16)
if (idm_16) == "0000000000000000":
time.sleep(1)
continue
# 読み取れた場合、環境変数ファイルでSlackのmember IDが紐づけられていないかどうか確認
member = os.getenv(idm_16)
if member is None:
# 紐付けがなかった場合、新規登録とみなして作成者に通知
slack.chat.post_message(ADMIN_ID, "{}が新しくカードをかざしました".format(idm_16), as_user=False)
else:
# 紐付けがあった場合、入室or退室の状態をテキストファイルから読み取り、メンションをつけてSlackへ投稿
f = open('state.txt', 'r')
state = f.read()
f.close()
slack.chat.post_message("#1108access_management","{}が{}しました".format(member,state_statement[state]) , as_user=False)
libpafe.free(felica)
time.sleep(5)
except KeyboardInterrupt:
libpafe.pasori_close(pasori)
exit()
Flaskサーバ
htmlファイルをホストするサーバです。
webページから発火される入室 / 退室の切替イベントを受け取って、状態保存用のテキストファイルを上書きしています。
環境
- Python 3.6
- Flask (サーバ用)
- Flask-SocketIO (htmlファイルとのSocketIO通信用)
後述するwebページにある入室/退室のボタンの切り替えは、SocketIOに載せて通知しています。
そのイベントの発火を受け取るために、Flask-SocketIOを使っています。
$ pip install Flask
$ pip install flask-socketio
プログラム
プログラム作成にあたって以下の記事を参考にさせて頂きました。
# coding: utf-8
from flask import Flask, render_template
from flask_socketio import SocketIO
app = Flask(__name__)
socketio = SocketIO(app)
# ルートにアクセスした際に表示するページ
@app.route('/')
def hello():
html = render_template('index.html')
print('connected')
return html
# htmlファイルからの'state_from_client'イベントを監視
# イベントを受け取ったら、入退室状態保存用のファイルを書き換える
@socketio.on('state_from_client', namespace='/')
def receive_message(msg):
f = open('state.txt', 'w')
f.write(msg)
f.close()
print(msg)
if __name__ == '__main__':
try:
socketio.run(app, host='0.0.0.0', port=5000)
except KeyboardInterrupt:
exit()
入退室切替用Webページ
入退室状態を選択するUIを表示するhtmlファイルです。
入室/退室のボタンを押すとイベントが発火され、SocketIOを通じてサーバに伝えられます。
環境
- jQuery 3.2.1
- Socket.IO 3.0.0
- Bootstrap 5.0.0
プログラム
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>入退室UI</title>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://cdn.socket.io/3.0.0/socket.io.min.js" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1"
crossorigin="anonymous">
<style>
.btn {
padding: 200px 120px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
font-size: 1.7rem;
}
</style>
</head>
<body>
<div class="container">
<div id="display-state-row" class="row text-center">
<p class="text-center mt-5 mb-0">現在の状態</p><br>
<h1 id="state" class="text-center mt-4 mb-5">入室</h1>
</div>
<div id="ui-buttons" class="d-flex justify-content-around mt-2">
<div>
<input type="radio" class="btn-check" name="options-outlined" id="enter" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="enter">入室</label>
</div>
<div>
<input type="radio" class="btn-check" name="options-outlined" id="leave" autocomplete="off">
<label class="btn btn-outline-success" for="leave">退室</label>
</div>
</div>
</div>
</body>
<script>
let socket = io.connect();
$('#enter').on('click', (e) => {
$('#state').html('入室');
socket.emit('state_from_client', 'enter');
});
$('#leave').on('click', (e) => {
$('#state').html('退室');
socket.emit('state_from_client', 'leave');
});
</script>
</html>
運用してみて
実際にこのシステムを運用してみて5ヶ月ほど経つので、いくつかアプデした点と今も問題となっている点について話します。
熱で落ちる問題
熱でラズパイが落ちてしまうことが何度もあったので、ちょっと見苦しいですが研究室にあったファンを置いてマステで固定して運用しています。また、落ちてもcronでプログラム強制再起動するようにしました。
その結果、連続稼働にも耐えています...がまた次の問題が (後述)
入室 / 退室の反映が重い問題
現在、入室 / 退室の切替フローが以下のようになっています。
- iPadに表示しているwebページの入室 / 退室をタップ
- Socket.IOのイベントを発火してサーバに通知
- サーバがイベントを受け取ったら、状態を記述しているテキストファイルを上書き
- 学生証をタッチしたらテキストファイルの状態を読んでSlackへPOST
この2→3の部分が重く、切り替えに15分以上かかってしまうことが多いです...
サーバを起動してすぐの間は問題なく動くのですが、しばらくすると重くなります。
SSH接続に関しても同じ症状なので、ラズパイが重くなってしまっているとみなして半ば諦めています😢
(有識者いらっしゃったら是非教えてください🙇♂️)
おわりに
ありがたいことに、研究室メンバーのうち21名に登録して頂き、現在5ヶ月以上運用しています🙏
実際に運用してみて色々と問題はありますが、入室するたびにSlackを開いて「作業します」という手間は省けたのはかなりのメリットだったのではないでしょうか(特に面倒くさがりな私にとって)。
退室時にリアクションをつけていたときとは違い、退室時間を記録できるようになったのもよかったと思います。
タッチした際に何のフィードバックもない状態でまだ不完全なので、「読み取ったら音を鳴らす」などの処理を追加して改善していきたいと思います。