Raspberry piとNFCリーダーで出退勤管理システムをつくった
背景
先日下記の記事を投稿しましたが、その後ICカードを使った出退勤管理システムをつくったので色々と書き留めておきたいと思います。
NFCリーダーのセットアップは下記で紹介いたしました。
環境構築
以下、環境構築です。
LINE Messaging API
出退勤管理を通知するにあたってLINEのMessaging APIを使用することにしました。基本的にこちら(Messaging API)から指示通りに進んで行けば登録が可能ですが、今回はPUSH MESSAGEを使用したいため、Developer Trialのアカウントを取得しました。下記のブログを参考にさせていただいております。
Django
LINE Messaging APIのWebhookリクエストの受け取りやPUSH MESSAGEの送信のためにPythonのWebフレームワークであるDjangoを利用しました。nfcpyがPython2系のみの対応だったためDjangoもPython2系の古いバージョンを使用しています。JSONをさばいたりHTTPリクエストを吐いたりするためにjsonとrequestsパッケージもインストールします。
$ sudo pip install django==1.11 json requests
導入から動作テストまではだいたい以下の記事を参考にさせていただいております。
Herokuにデプロイせずに以下で紹介するngrokでHTTPSにフォワーディングにして稼働させることにしました。*1
上記は、今後構成を変更するかもしれない。
ngrok
LINE Messaging APIはHTTPSのみのサポートのため、HTTPからHTTPSにトンネリングができるアプリケーションを導入します。Djangoのデフォルトのポート番号は8000ですからコマンドライン引数に8000を与えて起動しておきます。
$ wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip $ unzip ngrok-stable-linux-arm.zip $ ./ngrok http 8000
これでlocalhost:8000が表示されたURLにフォワーディングされています。
mysql
ICカードのIDmと利用者の名前を紐付けるためにmysqlを導入しました。もっと楽にできる方法はありそう。*2
アプリケーション作成
シュッとコードを書いて動くようにします。
linebot/urls.py
"""linebot URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.11/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url, include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^bot/', include('bot.urls')), ]
bot/urls.py
from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index, name="index"), url(r'^callback', views.callback), url(r'^nfcpush', views.nfcpush), ]
bot/views.py
# -*- encoding: utf-8 -*- from django.shortcuts import render from django.http import HttpResponse import json import requests import mysql.connector REPLY_ENDPOINT = 'https://api.line.me/v2/bot/message/reply' PUSH_ENDPOINT = 'https://api.line.me/v2/bot/message/push' ACCESS_TOKEN = '[ACCESS_TOKEN]' HEADER = { "Content-Type": "application/json", "Authorization": "Bearer " + ACCESS_TOKEN } PUSH_GROUP = "[GROUP_ID]" user_states = {} def index(request): return HttpResponse("This is bot api.") def callback(request): reply = "" request_json = json.loads(request.body.decode('utf-8')) print(json.dumps(request_json)) for e in request_json['events']: reply_token = e['replyToken'] message_type = e['message']['type'] if message_type == 'text': text = e['message']['text'] reply += reply_text(reply_token, text) return HttpResponse(reply) def reply_text(reply_token, text): if text.find(u"[検出したい文字列]") != -1: count = 0 users = [] for k, v in user_states.items(): if v == True: count += 1 users.append(k) reply_text = str(count) + u"人がオフィスにいます\n" dbcon = mysql.connector.connect( database="pi", user="root", password="[password]", host="localhost" ) for u in users: dbcur = dbcon.cursor() sql = "select name from cardinfo where id='" + u + "'" dbcur.execute(sql) row = dbcur.fetchone() if row == None: reply_text += u"未登録のユーザー " else: reply_text += row[0] + " " payload = { "replyToken":reply_token, "messages":[ { "type":"text", "text": reply_text } ] } requests.post(REPLY_ENDPOINT, headers=HEADER, data=json.dumps(payload)) return reply_text def nfcpush(request): request_json = json.loads(request.body.decode('utf-8')) dbcon = mysql.connector.connect( database="pi", user="root", password="[password]", host="localhost" ) dbcur = dbcon.cursor() sql = "select name from cardinfo where id='" + request_json['cardid'] + "'" dbcur.execute(sql) row = dbcur.fetchone() text = u"さんが" print(request_json['cardid'] in user_states) if not request_json['cardid'] in user_states: user_states[request_json['cardid']] = True text += u"出勤しました。" else: if user_states[request_json['cardid']]: text += u"退勤しました。" user_states[request_json['cardid']] = False else: text += u"出勤しました。" user_states[request_json['cardid']] = True name = "" if row == None: name = u"未登録のユーザー" else: name = row[0] payload = { "to":PUSH_GROUP, "messages":[ { "type":"text", "text": name + text } ] } requests.post(PUSH_ENDPOINT, headers=HEADER, data=json.dumps(payload)) return HttpResponse("OK")
すべての処理は bot/views.py
に記述してあります。
callback(request)
ではLINE Messaging APIのWebhookリクエストを捌きます。送られてきたテキストの先頭が任意の文字列であった場合に現在オフィスにいる人数と人の名前を返信します。
nfcpush(request)
ではNFCリーダーで読んだIDm入りのJSONを捌きます。DBでIDmから名前を確認し、未出勤であるなら"出勤しました"、出勤済であるなら"退勤しました"というPUSH MESSAGEを設定してあるグループに投稿するようにしてあります。
NFCリーダーからICカードを読み取ってリクエストを投げるスクリプトは以下です。
nfcpush.py
import binascii import nfc import json import requests HEADER = { "Content-Type": "application/json", "Authorization": "Bearer" } class MyCardReader(object): def on_connect(self, tag): print("touched") self.idm = binascii.hexlify(tag.idm) payload = { "cardid":self.idm } requests.post("http://localhost:8000/bot/nfcpush", headers=HEADER, data=json.dumps(payload)) return True def read_id(self): clf = nfc.ContactlessFrontend('usb') try: clf.connect(rdwr={'on-connect': self.on_connect}) finally: clf.close() if __name__ == '__main__': cr = MyCardReader() while True: print("touch card:") cr.read_id() idm = cr.idm print("released") print(cr.idm)
また、DBへの名前の登録は以下のスクリプトで行っています。
register.py
import binascii import nfc import json import mysql.connector class MyCardReader(object): def on_connect(self, tag): print("touched") self.idm = binascii.hexlify(tag.idm) return True def read_id(self): clf = nfc.ContactlessFrontend('usb') try: clf.connect(rdwr={'on-connect': self.on_connect}) finally: clf.close() if __name__ == '__main__': print("enter your name:") input_line1 = raw_input() cr = MyCardReader() print("touch card:") cr.read_id() idm = cr.idm dbcon = mysql.connector.connect( database="pi", user="root", password="[password]", host="localhost" ) dbcur = dbcon.cursor() sql = "INSERT INTO cardinfo (id, name) values ('" + idm + "', '" + input_line1 + "')" dbcur.execute(sql) dbcon.commit() print("released") print(cr.idm)
実行
うまく動作しました!!!
まとめ
名前の登録をするときは、 nfcpush.py
をいちいち止めないと行けないのが面倒。登録はいまはCUIのみなのでグラフィカルにいい感じにやりたい。
そもそも、ICカードをタッチするのが面倒なのでカメラで顔認識とかしてうまいことやりたい。