KAIKETSU Developer's Diary

株式会社KAIKETSU エンジニアチームのブログです

Raspberry piとNFCリーダーで出退勤管理システムをつくった

背景

先日下記の記事を投稿しましたが、その後ICカードを使った出退勤管理システムをつくったので色々と書き留めておきたいと思います。

NFCリーダーのセットアップは下記で紹介いたしました。

dev-kk2.hatenablog.com

環境構築

以下、環境構築です。

LINE Messaging API

出退勤管理を通知するにあたってLINEのMessaging APIを使用することにしました。基本的にこちら(Messaging API)から指示通りに進んで行けば登録が可能ですが、今回はPUSH MESSAGEを使用したいため、Developer Trialのアカウントを取得しました。下記のブログを参考にさせていただいております。

uzulla.hateblo.jp

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

導入から動作テストまではだいたい以下の記事を参考にさせていただいております。

qiita.com

Herokuにデプロイせずに以下で紹介するngrokでHTTPSフォワーディングにして稼働させることにしました。*1

上記は、今後構成を変更するかもしれない。

ngrok

LINE Messaging APIHTTPSのみのサポートのため、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

f:id:kk2dev:20180109170256p:plain これで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カードをタッチするのが面倒なのでカメラで顔認識とかしてうまいことやりたい。

*1:いま考えるとnfcpyだけ分割してDjangoはPython3系でHerokuにデプロイして動かすほうが賢いかもしれない。

*2:アホだったのでmysqlでやったけどDjangoからSQLiteとかを使ったほうが絶対に楽。なぜそうしなかったのか...