LoginSignup
11
12

学生証で行う入退室管理システム【全体プログラム】

Last updated at Posted at 2023-05-25

はじめに

近畿大学ロボット研究会の部室にFelica(学生証)を用いた入退室管理システムを作成しました。今回はプログラムの紹介をします。(これを作ったころは、動かすことを目指していたのでキレイなコードとは程遠いです)

入退室管理システムについて

入退室管理システムの全体像や動作のようすは以下のブログにまとまっています。参考にしてください。

このシステムと連動して、電気のスイッチを押すようなことまでやっています。

ファイル構成

入退室
┣ main.py
┣ inAA.py
┣ outAA.py
┣ club.xlsx
┣ voice.mp3
┣ guest.mp3
┣ arere.mp3
┗ piropiro.mp3

プログラム

全体像

まずはコメントを最小限にして、プログラム全部載せておきます。各部の解説は後に続きます。

import numpy as np
import binascii
import nfc
import time
import requests
from pygame import mixer
import pyttsx3
from gtts import gTTS
import webbrowser
import pyautogui
import discord
import json
import openpyxl

import inAA
import outAA


'''学生証のサービスコード'''
service_code = 0x1A8B #それぞれによる

'''トークンなどの初期設定'''
url = 'webhoockのURL' #自分のものを記入する

'''エクセルファイル'''
filename = 'club.xlsx'
sheetname = 'Sheet1'

'''起動して一回だけ実行する'''
def setup():
    try:
        discord_notify(url, "起動しました")
    except:
        net_connect()

'''学生証読み取ったときの処理'''
def on_connect_nfc(tag):
    try:
        '''学生証から必要な情報を手に入れるための設定'''
        idm, pmm = tag.polling(system_code=0xfe00)
        tag.idm, tag.pmm, tag.sys = idm, pmm, 0xfe00
    except:
        print('\n読み取れませんでした。もう一度お願いします。\n')
        error_call()
        time.sleep(1)
        main()

    '''学生証の型番だったら'''
    if isinstance(tag, nfc.tag.tt3.Type3Tag):
        try:
            '''学生証から学籍番号を取り出す'''
            sc = nfc.tag.tt3.ServiceCode(service_code >> 6 ,service_code & 0x3f)
            bc = nfc.tag.tt3.BlockCode(0,service=0)
            data = tag.read_without_encryption([sc],[bc])
            sid = int(data[2:12])
            '''エクセル内で検索'''
            student_line = excel_student_column(sid)
            '''ゲストモード'''
            if student_line == '0':
                guest_name, guest_call_name = guest_in(sid)
                inAA.inAA(guest_name)
                guest_call(guest_call_name, 'よろしくね')
                wb, ws = excel_file_read(filename, sheetname)
                n = sum_column(ws['D'], '1') + 1
                '''discord送信処理'''
                message_o = guest_name + 'が入室しました。\n現在部室に ' + str(n) + '人 います。\nーーーーーーーーーーーーーー'
                discord_notify(url, message_o)

            else:
                '''部員の情報を取得'''
                student_chinese_name = get_info('B', student_line)
                status = get_info('D', student_line)

                '''退室状態(0)のとき入室処理を行う'''
                if status == '0':
                    try:
                        '''エクセルデータを更新する'''
                        wb, ws = excel_file_read(filename, sheetname)
                        overwrite('D', student_line, '1')
                        now_in = time.time()
                        str_now_in = str(now_in)
                        overwrite('E', student_line, str_now_in)
                        n = sum_column(ws['D'], '1') + 1
                        '''discord送信処理'''
                        message_e = student_chinese_name + 'が入室しました。\n現在部室に ' + str(n) + '人 います。\nーーーーーーーーーーーーーー'
                        discord_notify(url, message_e)
                    except:
                        net_connect()
                        main()

                    '''部室アウトプット処理'''
                    name_call(student_line, "こんにちは。")
                    inAA.inAA(student_chinese_name)

                else:
                    try:
                        '''エクセルデータを更新する'''
                        wb, ws = excel_file_read(filename, sheetname)
                        overwrite('D', student_line, '0')
                        if_data = get_info("B", student_line)
                        if if_data != 'ゲスト' + str(sid):
                            '''時間計算'''
                            now_out = time.time()
                            str_now_out = str(now_out)
                            past = get_info('E', student_line)
                            active_time = float(now_out) - float(past)
                            overwrite('E', student_line, str_now_out)
                            sum_time = get_info('F', student_line)
                            new_sum_time = float(sum_time) + float(active_time)
                            overwrite('F', student_line, new_sum_time)
                        n = sum_column(ws['D'], '1') - 1
                        '''discord送信処理'''
                        message_o = student_chinese_name + 'が退室しました。\n現在部室に ' + str(n) + '人 います。\nーーーーーーーーーーーーーー'
                        discord_notify(url, message_o)
                    except:
                        net_connect()
                        main()

                    '''活動時間の表示'''
                    if if_data != 'ゲスト' + str(sid):
                        day1, hour1, minutes1, second1 = cal_time(active_time)
                        day2, hour2, minutes2, second2 = cal_time(new_sum_time)
                        print('今回の活動時間は' + str(day1) +'' + str(hour1) + '時間' + str(minutes1) + '' + str(second1) + '秒です。')
                        print('これまでの活動時間は' + str(day2) +'' + str(hour2) + '時間' + str(minutes2) + '' + str(second2) + '秒です。')
                    else :
                        hour1 = 25

                    '''部室アウトプット処理'''
                    outAA.outAA(student_chinese_name)

                    if hour1 == 0:
                        arere_call()
                    elif hour1 == 25:
                        guest_call('ゲスト', '。また来てね')
                    else:
                        name_call(student_line, "お疲れさまです。")

                    '''ゲストデータの削除'''
                    if if_data == 'ゲスト' + str(sid):
                        print(student_line)
                        row_clear(student_line)

        except Exception as e:
            error_call()
            print("error: %s" % e)

    else:
        error_call()
        print("error: tag isn't Type3Tag")


'''時間の計算'''
def cal_time(specific_time):
    day = int(specific_time // 86400)
    time_left = specific_time - 86400 * day
    hour = int(time_left // 3600)
    time_left2 = time_left - 3600 * hour
    minutes = int(time_left2) // 60
    time_left3 = time_left2 - 60 * minutes
    second = int(time_left3 // 1)
    return day, hour, minutes, second


'''音関係'''
def name_call(line, word):
    speak = get_info("C", line)
    call_word = speak +"さん" + word
    tts = gTTS(text= call_word, lang = "ja")
    tts.save('voice.mp3')
    mp3('voice.mp3')

def guest_call(name, word):
    call_word = name +"さん" + word
    tts = gTTS(text= call_word, lang = "ja")
    tts.save('guest.mp3')
    mp3('guest.mp3')

def error_call():
    mp3('piropiro.mp3')

def arere_call():
    mp3('arere.mp3')

def mp3(file_name):
    mixer.init()
    mixer.music.load(file_name)
    mixer.music.play()
    time.sleep(2)

'''discordの処理'''
def discord_notify(url, message):
    """
    Discord send message.

    Args:
        url (str): discord webhook url.
        message (str): message.
    """
    if url:
        requests.post(url,data={'content': message})

'''エクセル関数'''
def excel_file_read(file_name, sheet_name):
    filename = file_name
    wb = openpyxl.load_workbook(filename)
    ws = wb[sheet_name]
    return wb, ws

def row_clear(line):
    wb, ws = excel_file_read(filename, sheetname)
    ws.delete_rows(line)
    wb.save("club.xlsx")

def overwrite(column, line, inout):
    wb, ws = excel_file_read(filename, sheetname)
    '''シート内のA1セルに文字列を入力'''
    cell = column + line
    ws[cell] = inout
    wb.save("club.xlsx")

def sum_column(column, keyword):
    wb, ws = excel_file_read(filename, sheetname)
    result = 0
    for cell in column:
        try:
            value = str(cell.value)
        except:
            continue
        if value == keyword:
            result += 1
    return result

def get_info(specific_column, line):
    wb, ws = excel_file_read(filename, sheetname)
    student_information = specific_column + line
    info = ws[student_information].value
    info_str = str(info)
    return info_str

def guest_in(sid):
    wb, ws = excel_file_read(filename, sheetname)
    '''シート内のA列セルに文字列を入力'''
    str_sid = str(sid)
    do_check = 0
    for row in range(101, 200):
        if do_check == 1:
            break
        str_row = str(row)
        if ws['A' + str_row].value == None:
            ws['A' + str_row] = str_sid
            ws['B' + str_row] = 'ゲスト' + str_sid
            ws['C' + str_row] = 'ゲスト' + str_sid[0:2] + '' + str_sid[7:10] + ''
            ws['D' + str_row] = '1'
            do_check = 1
    wb.save("club.xlsx")
    '''エクセルに書き込んだのはすぐ使えないから処理用に準備'''
    make_name = 'ゲスト' + str_sid
    make_call_name = 'ゲスト' + str_sid[0:2] + '' + str_sid[7:10] + ''
    return make_name, make_call_name

def excel_student_column(sid):
    wb, ws = excel_file_read(filename, sheetname)
    '''学籍番号の列から何行目にあるのか検索する'''
    str_sid = str(sid)
    result = search_column(ws['A'], str_sid)
    if result == 0:
        str_result = '00'
    else:
        str_result = str(result)
    student_num = str_result[1:]
    return student_num


def search_column(column, keyword):
    result = 0
    for cell in column:
        try:
            value = str(cell.value)
        except:
            continue
        if value == keyword:
            cell_address = openpyxl.utils.get_column_letter(cell.column) +  str(cell.row)
            result = cell_address
    return result


'''インターネット接続処理'''
def net_connect():
    webbrowser.open("ログインするサイトのURL") #必要なら記入
    #以下は
    time.sleep(5)

    pyautogui.press("tab")
    time.sleep(1)
    pyautogui.write('学籍番号')

    pyautogui.press("tab")
    time.sleep(1)
    pyautogui.write('パスワード')

    time.sleep(1)

    pyautogui.press("tab")
    pyautogui.press("enter")
    time.sleep(5)


'''メイン処理'''
def main():
    clf = nfc.ContactlessFrontend('usb')
    while True:
        clf.connect(rdwr={'on-connect': on_connect_nfc})

if __name__ == "__main__":
    setup()
    main()

シーケンス図

かなり複雑なのでchatGPTにPlantUMLでシーケンス図をつくれるように指示しました。
主要な処理だけを含む形でこんな感じになります。(やったのはリンク上の言葉の修正のみ。上の汚いコードをツッコんだら主要なパートを取り出して整理してくれた!すごい!)

Screenshot 2023-05-25 at 2.14.27.png

入り組んだコードなのに主要なところをうまく取り出して表してくれています。

やり方は、

を参考にしました。

コード解説

ここからコードの詳細解説に入ります。まずはプログラムの大きな流れを説明します。その後に関数(部品)の説明をします。

ライブラリ

'''利用したライブラリ'''
import numpy as np #数値計算ライブラリ。
import binascii #バイナリと ASCII コード化されたバイナリ表現との間の変換を行うためのライブラリ。
import nfc #SONY製のUSB接続によるNFCリーダーを接続してNFCタグにアクセスするためのライブラリ。
import time #時間関連の情報や関数を集めたライブラリ。
import requests #HTTP通信用のPythonのライブラリ。
from pygame import mixer#音声読み上げ用のライブラリ。
import pyttsx3 #文字列読み上げのライブラリ。
from gtts import gTTS #文字列読み上げのライブラリ。
import webbrowser #ウェブを操作するライブラリ。
import pyautogui #キーボードやマウスの操作を自動化するライブラリ。
import discord #Discord用のライブラリ。
import json #JSON形式のデータを扱うためのライブラリ。
import openpyxl #エクセルを操作するためのライブラリ。

'''自作'''
import inAA #アスキーアートをためておくファイル。inAA.py
import outAA #アスキーアートをためておくファイル。outAA.py

コードの方にどのようなライブラリなのかを書いたので何ができるものなのかを補足しておきます。

ライブラリ 機能
numpy 高速かつ効率的な多次元配列や行列の操作が可能。
binascii バイナリと ASCII コード化されたバイナリ表現との間の変換を行うためのライブラリ。
nfc SONY製のUSB接続によるNFCリーダーを接続してNFCタグにアクセスするためのライブラリ。
time プログラム内で現在時刻を確認したり、処理を一時停止できる。
requests Web APIやWebサイトにリクエストを送信することができる。
pygame Pythonで音声ファイルを再生することができる。
pyttsx3 Pythonでテキストを音声に変換することができる。
gTTS Google Text-to-Speech APIを使用してテキストを音声に変換することができる。
webbrowser Pythonでブラウザを制御してWebページを開いたり、リンクをクリックしたりすることができる。
pyautogui PythonでGUIテストやタスク自動化を実現することができる。
discord PythonからDiscordのAPIを利用してボットを操作することができる。
json Pythonの辞書型やリスト型などのオブジェクトをJSON形式に変換したり、JSON形式のデータをPythonのオブジェクトに変換したりすることができる。
openpyxl エクセルを操作するためのライブラリで、Pythonでエクセルファイルを読み書きしたり、セルの書式設定やグラフの作成などを行うことができる。

結局discordのライブラリは、botからwebhoockに変えたので使いませんでしたが、開発の途中に通ったので、書き残しておきます。(requirementsでは外してます)

requirements.txt
numpy
binascii
nfcpy
pygame
pyttsx3
gTTS
requests
webbrowser
pyautogui
json
openpyxl
socket

pipでパッケージを一括インストールする用です。
念のためコマンドも。

pip install -r requirements.txt
自作ライブラリ 機能
inAA 入室時に表示するアスキーアートを保持する
outAA 退室時に表示するアスキーアートを保持する

プログラムの中にアスキーアートが混在すると煩雑になるため分けています。

定数

次に、定数を定義しておきます。

'''学生証のサービスコード'''
service_code = 0x1A8B #学生証によって変わる

'''discordの初期設定'''
url = 'webhoockのurlを入力' #discordのwebhoock

'''エクセルファイル'''
filename = 'club.xlsx' #エクセルファイル名
sheetname = 'Sheet1' #シート名

サービスコードの調べ方は、

こちらを参考にしてください。またnfcpyの公式ドキュメントやソニーの仕様書を見ることをおすすめします。

discordのwebhoockの用意は、

こちらを参考にしてください。

pythonでの送信は、

こちらが参考になります。

最後に読み込むエクセルファイルを定義します。

起動時

いよいよシステムを動かす部分です。

'''起動して一回だけ実行する'''
def setup():
    try:
        discord_notify(url, "起動しました") #discordに送信
    except:
        net_connect() #インターネット接続

プログラムを起動するとdiscordに「起動しました」とお知らせします。
実行する場所が大学なのでインターネット接続が定期的に切れるため、discordに送信できなかった場合はインターネット接続を試みる関数net_connect()を呼び出しています。

読み取り

def on_connect_nfc(tag):
    try:
        '''システムコード指定(指定しなければ最初に見つかった12FCが読まれる)'''
        idm, pmm = tag.polling(system_code=0xfe00)
        tag.idm, tag.pmm, tag.sys = idm, pmm, 0xfe00
    except:
        print('\n読み取れませんでした。もう一度お願いします。\n')
        error_call() #エラーの音楽流す
        time.sleep(1)
        main()

カードを読み取るときのシステムコードを指定して読み込みます。
読み込めなかったら、もう一度タッチするように促してエラーの音を流す関数を呼び出します。
このエラーがあるとプログラムが終了するので、終わらないようにmain()関数を呼び出して継続するようにしています。

関数
error_call()

学生証から学籍番号の取り出し

'''学生証の型番だったら'''
if isinstance(tag, nfc.tag.tt3.Type3Tag):
    try:
        '''学生証から学籍番号を取り出す'''
        #サービスコードの指定
        sc = nfc.tag.tt3.ServiceCode(service_code >> 6 ,service_code & 0x3f)
        #読取りブロックを指定(複数読むときは配列で指定する)
        bc = nfc.tag.tt3.BlockCode(0,service=0)
        data = tag.read_without_encryption([sc],[bc])
        #読む位置を指定
        sid = int(data[2:12]) #学籍番号 int型で手に入れる

このコードは以下のサイトを参考にしました。

最後取り出したデータの前後に不要な文字がくっついていたので、一部くり抜いて取り出しました。
後の利用を考えint型にして格納しています。

部員の取得

'''エクセル内で検索'''
student_line = excel_student_column(sid) #エクセル内の学籍番号を検索して行番号を手に入れる
'''ゲストモード'''
if student_line == '0': #名簿になかったら
    guest_name, guest_call_name = guest_in(sid) #ゲストのときのエクセル処理
    inAA.inAA(guest_name) #アスキーアート表示する
    guest_call(guest_call_name, 'よろしくね') #部室で音声が流れる
    wb, ws = excel_file_read(filename, sheetname) #エクセルファイルを読み込む準備
    n = sum_column(ws['D'], '1') + 1 #入室している人数を数える。
    '''discord送信処理'''
    message_o = guest_name + 'が入室しました。\n現在部室に ' + str(n) + '人 います。\nーーーーーーーーーーーーーー'
    discord_notify(url, message_o)

else:
    '''部員の情報を取得'''
    student_chinese_name = get_info('B', student_line) #名前(漢字)の取得
    status = get_info('D', student_line) #入退室状態の取得

部員の情報はエクセルに保存してあるため、まずエクセルファイルにアクセスして読み取った学生証を照合します。
照合できなかった場合はゲストとしてエクセルファイルに新たに書き込みます。
その後、部室の画面にアスキーアートが表示され、音声が流れ、discordにゲストの学籍番号と入室したことが知らされます。

部員の情報が照合できた場合は、登録している名前と入室状態を取得します。

関数
excel_student_column()
guest_in()
guest_call()
excel_file_read()
sum_column()
discord_notify()

入室処理

'''退室状態(0)のとき入室処理を行う'''
if status == '0':
    try:
        '''エクセルデータを更新する'''
        wb, ws = excel_file_read(filename, sheetname)
        overwrite('D', student_line, '1') #入室状態(1)に上書き
        now_in = time.time() #現在時間(エポック秒数)取得
        str_now_in = str(now_in) #文字列に変換
        overwrite('E', student_line, str_now_in) #活動時間に現在時間を上書き
        n = sum_column(ws['D'], '1') + 1 #入室している人数を数える(現在処理中のものが反映されないので+1しておく)
        '''discord送信処理'''
        message_e = student_chinese_name + 'が入室しました。\n現在部室に ' + str(n) + '人 います。\nーーーーーーーーーーーーーー'
        discord_notify(url, message_e)
    except:
        net_connect()
        main()
    '''部室アウトプット処理'''
    name_call(student_line, "こんにちは。") #名前読み上げる
    inAA.inAA(student_chinese_name) #アスキーアート表示する

入室状態を1、退室状態を0にしているので、取得した状態が0の時入室処理を行います。

入室状態にエクセルファイルを上書き→入室した時間を取得してエクセルファイルに書き込む→入室している人数を取得→discordに送信

こちらも入室時にインターネットが切れていたらネットを繋げる処理を入れています。

部室では入室処理が完了すると
名前が呼ばれ挨拶される→画面にウェルカムメッセージのアスキーアートが表示される
このようなイベントが起きます。

関数
excel_file_read()
overwrite()
sum_column()
discord_notify()
net_connect()
name_call()

退室処理

else:
    try:
        '''エクセルデータを更新する'''
        wb, ws = excel_file_read(filename, sheetname)
        overwrite('D', student_line, '0') #入退室状態上書き
        if_data = get_info("B", student_line)
        if if_data != 'ゲスト' + str(sid): #ゲストじゃない時
            '''時間計算'''
            now_out = time.time() #現在時間(エポック秒数)取得
            str_now_out = str(now_out)
            past = get_info('E', student_line)
            active_time = float(now_out) - float(past) #活動時間
            overwrite('E', student_line, str_now_out) #活動時間に上書き
            sum_time = get_info('F', student_line)
            new_sum_time = float(sum_time) + float(active_time)
            overwrite('F', student_line, new_sum_time) #累計時間に上書き
        n = sum_column(ws['D'], '1') - 1#入室している人数を数える
        '''discord送信処理'''
        message_o = student_chinese_name + 'が退室しました。\n現在部室に ' + str(n) + '人 います。\nーーーーーーーーーーーーーー'
        discord_notify(url, message_o)
    except:
        net_connect()
        main()

'''活動時間の表示'''
if if_data != 'ゲスト' + str(sid): #ゲストじゃない時
    day1, hour1, minutes1, second1 = cal_time(active_time) #活動時間をUNIX時間から変換
    day2, hour2, minutes2, second2 = cal_time(new_sum_time) #累積活動時間をUNIX時間から変換
    print('今回の活動時間は' + str(day1) +'' + str(hour1) + '時間' + str(minutes1) + '' + str(second1) + '秒です。')
    print('これまでの活動時間は' + str(day2) +'' + str(hour2) + '時間' + str(minutes2) + '' + str(second2) + '秒です。')
else :
    hour1 = 25 #ゲストを区別するため

'''部室アウトプット処理'''
outAA.outAA(student_chinese_name) #退出時AAモジュール

if hour1 == 0:
    arere_call() #時間短かったら「あれれ〜」流す
elif hour1 == 25:
    guest_call('ゲスト', '。また来てね') #ゲストには「また来てね」
else:
    name_call(student_line, "お疲れさまです。") #お疲れ様音声

取得した状態が1(入室状態)の時、退室処理を行います。

退室状態にエクセルファイルを上書き→退室した時間を取得してエクセルファイルに書き込む→入室している人数を取得→discordに送信

こちらも入室時にインターネットが切れていたらネットを繋げる処理を入れています。

部室では退室処理が完了すると
画面にねぎらいメッセージのアスキーアートが表示される→活動時間と累積活動時間が画面に表示される→名前が呼ばれねぎらいの言葉がかけられる
このようなイベントが起きます。

ゲストの場合は、時間の処理をパスするようにしています。

関数
excel_file_read()
overwrite()
get_info()
sum_column()
discord_notify()
net_connect()

ゲスト後処理

'''ゲストデータの削除'''
if if_data == 'ゲスト' + str(sid): #名前こいつのとき
    print(student_line)
    row_clear(student_line) #行を削除する関数

ゲストのデータをエクセルファイル上から消します。

関数
row_clear()

例外処理

'''学生証の型番だったら'''
if isinstance(tag, nfc.tag.tt3.Type3Tag):
    try:
        
        
        
    except Exception as e: #上記に当てはまらず例外だったらエラーと表示する
        error_call()
        print("error: %s" % e)

else: #読み込んだカードがタイプ3じゃなかったら
    error_call()
    print("error: tag isn't Type3Tag")

if文とtry文も補足して全体が見えるようにしています。

関数
error_call()

作った関数

関数は以下ににまとめます。

各プログラムで出てきた関数もまとめているので、そこからもブログにとべます。

メイン処理

'''メイン処理'''
def main():
    clf = nfc.ContactlessFrontend('usb')
    while True:
        #NFCリーダーに接続し、接続が確立された後にon_connect_nfc関数が呼び出される
        clf.connect(rdwr={'on-connect': on_connect_nfc})

if __name__ == "__main__":
    setup()
    main()

メインはNFCリーダーにアクションが起きたときにプログラムが動くように作っています。

おわりに

とても煩雑なコードですが、学生証を用いて行う入退室のシステムができました。今度chatGPTでリファクタリングしてみます。

11
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
12