NFCを用いて入室管理システムの作成備忘録Ver1.0

# システム作成の経緯
先日メリカリを閲覧していたところ、「Sony Corporation©︎ RC-S330」を格安で入手することができました。
そこで、NFCを用いて遊んでみようと思い入室管理システムの開発を行いました。
※今購入するのであれば、「Sony Corporation©︎ RC-S380S」が最新モデルなのでこちらを買うと良いと思います。

# 開発環境
* OS    : macOS Monterey12.2.1(Intel Mac)
* リーダ : Sony CorporationRC-S330
* 言語   : Python3.10.0
* DB    : AWS RDS(MySQLを使用)

# 搭載する機能の決定
入退室管理システムで必要な機能をまとめました。
1. 新規登録機能（ICカードの情報と名前等を登録する機能）
2. 削除機能（ユーザーの削除を行う機能）
3. 入退室記録（何時にどこに入退室したのか）

まずはシンプルに基本機能である上記3点の機能を実装することとします。

# システム構成図 & システムフロー

1. 新規登録機能のシステムフロー
![new-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/886848/23b6cd62-be6e-623e-820b-e5f2d9064494.png)

【説明 - 新規登録】
* アプリを作成します。
WEBアプリでも良いと思いますが、今回はpythonのGUIアプリを勉強したくGUIライブラリであるKivyを使用して作成を行います。
* WEBアプリ上から管理者ページにログインし、新規登録を行います。
その際に、ICカードを読み込みIDmを取得します。（IDmを選定した理由は後ほど記載）
* 取得した情報をDBに記録します。
* 最後に、web画面に登録完了の旨を表示します。

1. 削除
![delete-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/886848/aa053915-3f58-32dc-d8e0-352c632b5d8c.png)
【説明 - 削除】
* 管理者ページより削除ページに以降し、ICカードリーダーよりIDmを取得該当するデータの削除を実施します。
* 削除完了の旨の画面に表示します。

1. 入退室記録
![check-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/886848/5792a890-e348-ad55-3bd5-639a093afb2b.png)
【説明 - 入退室記録】
* リーダにICカードをかざし、IDmを取得します。
* 一致するIDmの入室時刻に、現在時刻を打刻します。
* 退出時は一致するIDmに退出時刻を打刻します。

# IDmとは（NFCについて軽く知識を）
システムを作成していくにあたってNFCの知識が必要になります。
そこで、NFCについて軽くまとめてみようと思います。

### NFCとは
この記事を読んでいる方の大半はNFCについて軽い知識はあると思うので、詳しい説明は外部の記事に任せます。

https://www.sony.co.jp/Products/felica/NFC/

Qiitaでわかりやすく、詳しく説明されている方がいたので引用させていただきました。
NFCの詳細について知りたい方はこちらの記事をぜひ読んでみてください。

https://qiita.com/gpsnmeajp/items/49db2212e632869edaf8

### IDmとは
フローや説明の中で、出てきた「IDm」について説明をします。
IDmはICチップ製造時に付与される固有の識別IDです。重複が発生しないようにSONYが管理を行なっています。
そのため、重複が発生しないことに加え、IDmを比較的簡単に読み込むことができるため幅広く活用されています。
そのため、今回の入退室管理システムではIDmを用いて管理を行うものとしました。
こちらのサイトの内容でIDmの説明を詳しくされています。私もこちらの記事を読みIDmでの実装を決めました。

http://www.orangetags.jp/words/idm

# 環境構築
これまでに、システムの仕様などが決まりました。いよいよ開発に移ります。
まずは環境構築を行います。
### Macの環境構築

下記の外部モジュールをインストールしてください。
(自分の環境ではPython2系と3系が入っているので、pip3で3系にインストールします。)
```外部モジュール
pip3 install Flask　　# flaskを使用するためのモジュール
pip3 install nfcpy　　# nfcリーダーを仕様するためのモジュール
pip3 install boto3　　# AWSを仕様するためのモジュール  　
pip3 install mysqlclient　　# mySQLをPythonで仕様するためのモジュール
pip3 install binascii　　# バイナリとASCIIを変換するためのモジュール
```
下記の中身はbashで記載されています。
インストールにはhomebrewを使ってインストールを行うためインストールされてない方は便利なので、この際にぜひインストールをしてみてください。
インストール方法は下記のqiita記事を参考にしてみてください。

https://qiita.com/zaburo/items/29fe23c1ceb6056109fd

```外部モジュール
brew install lsusb　　# ICリーダーが認識されているか確認するため
```
windowsの場合はコントロールパネルのデバイスマネージャーから確認できると思います。（筆者未確認）

### AWS(DB)の環境構築
こちらのページに全ての手順が記載されていましたので、下記のサイトの手順で行なってみてください。

https://dev.classmethod.jp/articles/sales-rds-ec2-session/

※接続ができない等の場合はセキュリティールールーやIAMの権限を見てみてください。
（自分はIAM権限を間違えており15分ほどハマりました。）

設定後EC2経由でRDSにアクセスを行いDBの設定を行なっていきます。
1. EC2に接続する
Macのターミナルから下記のコマンドを実行（ssh接続を行います。）

```EC2接続
ssh -i "自分決めた名前.pem" ec2-user@ec2-XX-XX-XXX-XX.ap-northeast-1.compute.amazonaws.com
```
※ 一部セキュリティーの観点からXXで隠しています。
途中で接続しても良いか聞かれるので「yes」と入力しましょう。
最終行に下記のように表示されたら、接続完了です。
```EC2接続完了
[ec2-user@ip-XXX-XX-XX-XXX ~]$ 
```
※ XXの部分にIPアドレスが入ります。
1. EC2上でmySQLを使えるようにします。
```mySQLインストール
sudo yum install mysql
```

1. RDSに接続する
EC2に接続された状態で下記のコマンドを実行
```EC2接続
mysql -u admin -p -h [RDSのエンドポイント]
```
※ [　]は必要ありません。

ここまでできたら環境構築は8割終了です。残るはデータベースの作成のみです。
簡単なSQL文しか使わないでSQLをやったことがない人でも大丈夫です！
詳しく知りたい方や、SQL文の意味を知りたい方はこちらの記事を参考にしてみてください！

https://qiita.com/tatsuya4150/items/69c2c9d318e5b93e6ccd

1. CREATE文を利用して、データベースを作ります。
下記のSQL文を実行して「headquarters」というデータベースを作成します。
データベースで本部の入退室管理、支店の入退室管理のように分ける感じで管理をしていきたいと思います。
```SQL
CREATE DATABASE headquarters;
```
上記コマンドを実行し、無事作成することができると下記のようなメッセージが表示されます。
```SQL
MySQL [(none)]> CREATE DATABASE headquarters;
Query OK, 1 row affected (0.03 sec)
```
本当に作成ができているか確認をしてみましょう。
下記のSQL文を入力してみてください。そうすることで現時点で作成されているデータベースを確認することができます。
```SQL
SHOW databases;
```
Databaseの中に「headquarters」というデータベースが作成されていることを確認できました。
```SQL
MySQL [(none)]> SHOW databases;
+--------------------+
| Database           |
+--------------------+
| headquarters       |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)
```

1. 使用するDBを変更する
DBの作成の次は使用するデータベースに移動する必要があります。
変更を行うには次のSQL文で行います。
```SQL
USE headquarters;
```
変更が完了すると
```SQL
Database changed
MySQL [headquarters]> 
```
と表示され、しっかりと変更されていることを確認することができます。

1. 次に、CREATE文を使用して、テーブルを作ります。
今回のシステムでは、部屋ごとにテーブルをしますのでテーブル名は部屋の名前とします。
例) 会議室1の場合 ⇒ meeting_room1
実際に作成する前に、どのようなテーブルをするのかをイメージしておきましょう。
今回このようなテーブルを作成します。

| IDm | name | in_time | out_time |status|
|:-:|:-:|:-:|:-:|:-:|
| 02xxxxxxxxxxxx19  | 山田　太郎  | 2022-03-04 15:54:20  | 2022-03-04 16:30:43  |      0    |
| 01xxxxxxxxxxxx24  | 田中　雄大  | 2022-03-04 16:15:54  | 2022-03-04 17:45:10  |      0    |

一部例を交えて作成してみました。これをSQL文で作成すると、下記のコードになります。
```SQL
create table room1(IDm varchar(20) NOT NULL, name TEXT NOT NULL, in_time datetime, out_time datetime, status INT primary key(IDm));
```
こちら正常に処理が完了すると、作成完了の旨が表示されます。
本当に作成できたかを確認していみます。現状では中身のデータが何一つとして入っていない状態なので、SELECT文では表示を行っても、Empty set (0.00 sec)と表示されます。
なので、DESCを使用してテーブルの定義情報を見てみます。
```SQL
MySQL [headquarters]> DESC room1;
+----------+-------------+------+-----+---------+-------+
| Field    | Type        | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+-------+
| IDm      | varchar(20) | NO   | PRI | NULL    |       |
| name     | text        | NO   |     | NULL    |       |
| in_time  | datetime    | YES  |     | NULL    |       |
| out_time | datetime    | YES  |     | NULL    |       |
| status   | int         | YES  |     | NULL    |       |
+----------+-------------+------+-----+---------+-------+
5 rows in set (0.00 sec)
```
このようにしっかりとField,Typeなどが定義されていることが分かります。
テーブルが完成したので、サンプルとしてデータを入れてみたいと思います。
INSERT文を使用します。
```SQL
INSERT INTO room1 VALUES ("0XX60XX0XXXBCXXB", "山田　太郎", "2019-05-02 12:48:35", "2022-03-06 02:57:35",0);
```
正常に完了ができたら実際にデータの登録ができたかを確認してみます。
SELECT文を使用して確認してみます。
```SQL
MySQL [headquarters]> SELECT * from room1;
+------------------+-----------------+---------------------+---------------------+--------+
| IDm              | name            | in_time             | out_time            | status |
+------------------+-----------------+---------------------+---------------------+--------+
| XX60XX0XXXBCXX0B | 山田　太郎        | 2019-05-02 12:48:35 | 2022-03-06 02:57:35 |      0 |
+------------------+-----------------+---------------------+---------------------+--------+
1 rows in set (0.00 sec)
```
結果の通り登録できていることがわかります。
これでAWS(DB)の構築は完了です。

# コーディング
ここまで、設定等を行ってきましたが、いよいよ本題？のコーディングです。
### 入退室記録のコード
入退室管理はPythonのみで行なっています。
これが最適な書き方ではないと思いますが、全体の設計を手を抜いてしまったせいで欠陥部分をコーディング中に発見してしまったため、コードを構想からやり直そうと思うので、一旦これでいきます。
※欠陥部分に関しては最後に記載
```Python
import nfc
import binascii
import boto3
from datetime import datetime
import MySQLdb
import time

connection = MySQLdb.connect(
host="AWS RDSのエンドポイント", 
user="admin", 
password="mysqlのパスワード",
db='headquarters',
charset='utf8')

print("読み込み待機中")
def addInOutTime():
    # inに入力か、outに入力かを判定
    # 該当する項目に現在日時をdatetime型で記録
    cursor = connection.cursor()
    cursor.execute("SELECT * from room1 WHERE IDm = " + "'" + idm + "'")
    get_info = cursor.fetchall()
    get_info[0][2] #in_timeの値を取得
    now_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")


    if get_info[0][2] != None and get_info[0][3] != None:
        #入室,退出ともに時刻が記録されている場合
        cursor.execute("UPDATE room1 SET in_time = NULL WHERE IDm = " + "'" + idm + "'") #データをNULLに上書き
        cursor.execute("UPDATE room1 SET out_time = NULL WHERE IDm = " + "'" + idm + "'") #データをNULLに上書き
        cursor.execute("UPDATE room1 SET in_time = " + "'" + now_time +  "'" + " WHERE IDm = " + "'" + idm + "'") #in_timeを打刻
        cursor.execute("UPDATE room1 SET status = 1 WHERE IDm = " + "'" + idm + "'") #ステータスを入室に変更
        cursor.close()
        connection.commit()
        print("入室処理完了")
        
    elif get_info[0][2] == None:
        #入室のデータがない場合 ⇒ 新規入室
        cursor.execute("UPDATE room1 SET in_time = " + "'" + now_time +  "'" + " WHERE IDm = " + "'" + idm + "'") #入室時刻を追加
        cursor.execute("UPDATE room1 SET status = 1 WHERE IDm = " + "'" + idm + "'") #ステータスを入室に変更
        cursor.close()
        connection.commit()
        print("入室処理完了")

    else:
        #退出のデータがない場合 ⇒ 退出
        cursor.execute("UPDATE room1 SET out_time = " + "'" + now_time +  "'" + " WHERE IDm = " + "'" + idm + "'")
        cursor.execute("UPDATE room1 SET status = 0 WHERE IDm = " + "'" + idm + "'") #ステータスを入室に変更
        cursor.close()
        connection.commit()
        print("退出処理完了")

def get_IDm():
    global idm
    try: 
        #USB接続
        clf = nfc.ContactlessFrontend('usb') # リーダーの接続確認
        tag = clf.connect(rdwr={'on-connect': lambda tag: False})
        idm = binascii.hexlify(tag.identifier).upper()
        idm = idm.decode()  
                                             
    except AttributeError:
        print("error")

while True:
    get_IDm()
    addInOutTime()
    time.sleep(3)
```
入室記録をとりことはとりあえずできるようになりました。
この先は現在制作中のため、後日追記しますね。