見出し画像

入退室管理システムを自作してみた

経緯
所属大学に、複数の研究室を寄せ集めて作った研究拠点が新しくできました。
学生や教職員の出入りが激しく、安全上の観点からも人の出入りを可視化したいという要望があったので、いわゆる"入退室管理システム"というやつを自作してみました。


図. 入退室管理システムのイメージ

材料
システム本体
Raspberry Pi 4 Model B 4GB
SanDisk microSDカード 128GB UHS-I Class10 (OS書き込み用)
Miuzei Raspberry Pi 4 ケース
SONY PaSoRi RC-S380/S

NFCカード類
FeliCa Lite-S 【10枚セット】
エーワン IDカード 作成キット カード10シート

本当はRaspberry Pi 5とか買えたら良かったのですが、マイコンに15kオーバーも出したくないのでPi 4のメモリ4GBで我慢。
Raspberry Pi用のケースはなくても動きますが、あった方が断然便利です。

NFCのカードリーダーは何も下調べをしないでRC-S300を購入したところ、nfcpy(今回使用予定のnfcであれこれするpython ライブラリ)が使えないことが判明。この手の工作はRC-S380/Sがおすすめです。
ただし、ソニーストアでの販売は終了しているので、早めに買っておいた方がいいかも。


作り方
Raspberry Pi セットアップ
Raspberry Pi Imagerを使ってmicro sdカードにOSを書き込みます。
今回はRaspberry Pi OS (64-bit)Bookwormを入れました。

Python環境構築
環境構築の方法はいくつかありますが、pythonのバージョンやライブラリのバージョンのconflictを回避するためにも仮想環境の構築推奨です。
仮想環境の構築方法はいくつかありますが、今回はみんな大好きAnacondaディストリビューションの軽量版、miniforgeを使いました。(参照

Python 3.11.9で仮想環境を構築し、nfcpyをインストール。

pip install nfcpy

lsusbを実行すると、PaSoRiが認識されていることが分かります。
ただし、仮想環境からusb deviceへのアクセス権がないので、udevのrulesファイルを書き換える必要があります。

sudo usermod -aG plugdev "user-name"

sudo vim /etc/udev/rules.d/99-usb.rules

これを書き込む↓

SUBSYSTEM=="usb", MODE="0666", GROUP="plugdev"
sudo udevadm control --reload-rules

sudo udevadm trigger

これで仮想環境からusb deviceにアクセスできるようになりました。


次に入退室記録の共有方法です。
ファイルを共有する方法はいくつかありますが、sambaのようなファイルサーバーを構築するやり方では、ファイルを見るPCが同じネットワークに接続されている必要があります。
これでは、離れた研究棟から入退室状況を知りたい時に困るので、Google driveに共有することにしました。
PyDrive2というPythonライブラリのがあるのでこれを使いました。
参照
※Raspberry Piの環境からシェル越しにGoogle アカウントの認証ができなかったので、別環境(Mac OS)で認証して出てきたcredentials.jsonファイルをRaspberry Pi環境にコピーするという、やや強引なやり方で解決しました。

プログラム作成
以下に今回使用したプログラムを記します。

getid.py

import nfc
clf = nfc.ContactlessFrontend('usb')
tag=clf.connect(rdwr={'targets': ['212F'], 'on-connect': lambda tag: False})
print(tag)

allrun.py

import subprocess
import csv
from datetime import datetime
import os
import time
import pandas as pd
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive

def main():
    # 現在の時刻を取得
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    # getid.pyを実行してタグ情報をoutput.txtに保存
    subprocess.run(["python", "getid.py"], stdout=open("output.txt", "w"))

    # output.txtからタグ情報を読み込む
    with open('output.txt', 'r') as file:
        tag_info = file.readline().strip()  # 改行を除去して1行目を読み込む

        # タグ情報を半角スペースで区切り、リストにする
        tag_info_list = tag_info.split(' ')

        # 実行時刻をリストの最初に追加
        tag_info_list.insert(0, current_time)

        # リストをCSVファイルに書き込む
        with open('reg.csv', 'a', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(tag_info_list)
            print("Tag information with current time has been written to reg.csv.")

        # data.csvから対応するNameを探す
        found_name = None
        with open('data.csv', 'r', encoding='utf-8') as data_file:
            data_reader = csv.reader(data_file)
            for row in data_reader:
                if row[1] == tag_info_list[5]:  # data.csvの2列目とreg.csvの6列目が一致するかチェック
                    found_name = row[0]  # 対応するNameを取得
                    break

        # 日付ごとのフォルダを作成
        folder_name = datetime.now().strftime("%y%m%d")
        if not os.path.exists(folder_name):
            os.makedirs(folder_name)

        # 日付ごとのファイルに時刻と対応するNameを書き込む
        file_path = os.path.join(folder_name, f"{folder_name}_record.csv")
        with open(file_path, 'a', newline='', encoding='utf-8') as reg1_file:
            reg1_writer = csv.writer(reg1_file)

            # 偶数回か奇数回かを判定してenterまたはexitを書き込む
            count = count_name_occurrences(file_path, found_name)
            status = 'enter' if count % 2 == 0 else 'exit'
            reg1_writer.writerow([current_time, found_name, status])

        print("Time, name, and status have been written to the respective CSV file.")

        # 2番目のスクリプトを呼び出す
        process_csv(folder_name)
        
        # ステータスCSVファイルをGoogle Driveにアップロード
        upload_to_google_drive(folder_name)

def count_name_occurrences(file_path, name):
    # ファイル内で名前が出現した回数を数える
    count = 0
    with open(file_path, 'r', newline='', encoding='utf-8') as file:
        reader = csv.reader(file)
        for row in reader:
            if len(row) > 1 and row[1] == name:
                count += 1
    return count

def process_csv(folder_name):
    # CSVファイルのパス
    file_path = f"/home/rei/Desktop/nfc/{folder_name}/{folder_name}_record.csv"

    # CSVファイルが存在しない場合は処理をスキップ
    if not os.path.exists(file_path):
        print(f"CSVファイルが見つかりません: {file_path}")
        return

    # CSVファイル読み込み
    data = pd.read_csv(file_path, header=None)

    # Nameごとにenterとexitの数を数える
    name_counts = data.groupby(1)[2].value_counts().unstack(fill_value=0)

    # 'exit' 列が存在するかどうかを確認
    if 'exit' in name_counts.columns:
        # enterの数引くexitの数が1以上のNameを抜き出す
        selected_names = name_counts[name_counts['enter'] - name_counts['exit'] > 0].index.tolist()
    else:
        # 'exit' 列が存在しない場合は全ての名前を選択する
        selected_names = name_counts.index.tolist()

    # 日付_status.csvファイルを作成し、選択されたNameを書き込む
    status_file_path = f"/home/rei/Desktop/nfc/{folder_name}/{folder_name}_status.csv"
    pd.DataFrame(selected_names, columns=["Name"]).to_csv(status_file_path, index=False, header=False)

def upload_to_google_drive(folder_name):
    # Google ドライブに接続
    gauth = GoogleAuth()
    gauth.LocalWebserverAuth()  # ローカルWebサーバーを作成し、自動的に認証を処理します。
    drive = GoogleDrive(gauth)

    file_name = f"{folder_name}_status.csv"
    status_file_path = f"/home/rei/Desktop/nfc/{folder_name}/{folder_name}_status.csv"

    # 同じ名前のファイルがすでに存在する場合は上書きする
    existing_files = drive.ListFile({'q': f"title = '{file_name}'"}).GetList()
    if existing_files:
        existing_file = existing_files[0]
        file = drive.CreateFile({'id': existing_file['id']})
        print("既存のファイルを上書き:", existing_file['id'])
    else:
        file = drive.CreateFile()
        print("新しいファイルを作成")

    file['title'] = file_name
    file.SetContentFile(status_file_path)

    try:
        file.Upload()
        print("ファイルがアップロードされました。ファイルID:", file['id'])
    except Exception as e:
        print("ファイルのアップロード中にエラーが発生しました:", e)

if __name__ == "__main__":
    while True:
        main()
        time.sleep(3)


今回のシステム構築にかかった費用は2万円ほど。
入退室管理システムをちゃんと導入しようとすると数十万〜かかるそうなので(参照)、かなり安く済ませることができたと思います。

いずれは、顔認証で入退室を管理するシステムなんかも実装してみたいですね。

この記事が気に入ったらサポートをしてみませんか?