BeaconやRFIDを使って自分で自分を監視する ②RFID編

この記事は
[ビジネスエンジニアリング株式会社（B-EN-G）アドベントカレンダー2024](https://qiita.com/advent-calendar/2024/b-en-g "B-EN-G アドベントカレンダー2024")の記事です。

 # はじめに
 当記事は、二部構成からなる記事の後編です。
 前編に目的や機材構成・システム全体図などを記載しています。
 [前編：BeaconやRFIDを使って自分で自分を監視する ①Beacon編](https://qiita.com/fujitarou/items/84d4cab5bd74e0f9c064)

 # RFID連携
### 1. RFIDリーダー(兼ライター)の組付け
ラズパイにブレッドボード経由でRFIDリーダーライターを組み付けます。
RFIDのセットは、RFIDリーダーライタモジュール、カード型RFIDタグ、キーホルダー型RFIDタグ、接続端子2種で構成されていました。
![rfid_1-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/dcc3fbb9-5ffa-80a6-515b-87ebea1e7a46.jpeg)
基盤に接続端子をはんだ付けしてブレッドボードに挿して、キットの[マニュアル](https://github.com/WayinTop/C-Python-Java-JP/blob/master/Raspberry%20Pi%E7%94%A8%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC%E3%82%AD%E3%83%83%E3%83%88%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%20C%2B%2B%20Python%20Java-JP.zip)の「Lesson28 RFID RC522」通りに配線していきます。
![rfid_2-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/e80ebe33-8b0c-67be-c80d-9056af1d4731.jpeg)
はんだ付けするとき、隣のピンとの間隔が結構狭くて、接触しないかとヒヤヒヤしましたがなんとかなりました。
~~まぁ失敗しても数百円で買えるっていうのは心強かった。~~
見ての通り基盤むき出しなので、勉強用って感じですね。

### 2. ラスパイの設定
こちらもマニュアルに沿って設定しました。
SPIなどのインターフェースの有効化、各種モジュールのインストールです。

### 3. サンプルコードでの動作確認とカードへの社員番号の書き込み
サンプルコードは[こちら](https://github.com/WayinTop/C-Python-Java-JP/blob/master/code.zip)。
今回はPythonのコードを使用しました。
コードの説明とRC522というデバイスの仕様がマニュアルに書いてあるので、読みつつ動かしつつ理解を深めていきました。
scan、read、dump、write、halt、既定のコマンドを使ってデータを読み書きします。
今回はSector16を使うことにしました。17も入れたものの使ってません。
![rfid3-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/d294f737-d3de-a5b2-480f-63b96ede44d9.png)

### 4. RFID連携プログラムの開発
サンプルでは、RFIDタグを置いてscanコマンドでしたが、タグを置いたら自動で読み込んでアクションを起こす形にしたいなと思い、
サンプルをベースに常にscanし続けるような実装にしました。
```python
import RPi.GPIO as GPIO
import MFRC522
import sys
import os
import requests
import json
import time


# Create an object of the class MFRC522
mfrc = MFRC522.MFRC522()

SECTOR_MAP = {
    1: [4, 5, 6],
    2: [8, 9, 10],
    3: [12, 13, 14],
    4: [16, 17, 18],
    5: [20, 21, 22],
    6: [24, 25, 26],
    7: [28, 29, 30],
    8: [32, 33, 34],
    9: [36, 37, 38],
    10: [40, 41, 42]
}


def read_addr_for_order(mfrc, card_id, sector):
    read_blocks = SECTOR_MAP[sector]
    text_data = {}
    # This is the default key for authentication
    key = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]
    for block_addr in read_blocks:
        # Authenticate
        status = mfrc.MFRC522_Auth(mfrc.PICC_AUTHENT1A, block_addr, key, card_id)
        # Check if authenticated
        if status == mfrc.MI_OK:
            counter = 0
            while True:
                text = mfrc.MFRC522_Readstr(block_addr)
                if text is None:
                    print(str(block_addr) + " is None.")
                    counter += 1
                    if counter < 100:
                        continue
                    return 0
                break
            text = text[0:40]
            text = text.replace("\x00", "")
            text_data[str(block_addr)] = text
            print(text_data)
        else:
            print("Authentication error")
            return 0
    return text_data

def send_data_at_put_card(data):
    print(json.dumps(data))
    url = "http://192.168.1.9:8123/rfid-human/device/device001" 
    headers = {'Content-Type': 'application/json'}
    
    response = requests.post(url, data=json.dumps(data), headers=headers)
    if response.status_code == 200:
        print("Data successfully sent to server.")
    else:
        print("Failed to send data to server. Status code:", response.status_code)


def send_data_at_remove_card():
    url = "http://192.168.1.9:8123/rfid-human/device/device001"
    headers = {}
    response = requests.delete(url, headers=headers)
    if response.status_code == 200:
        print("RC-Data successfully sent to server.")
    else:
        print("Failed to send data to server. Status code:", response.status_code)


def loop():
    global mfrc
    isSendDone = False
    isSetOnCard = False
    set_retry_count = 0
    while True:
        # Scan for cards
        (status,TagType) = mfrc.MFRC522_Request(mfrc.PICC_REQIDL)
        # If a card is found
        if status == mfrc.MI_OK:
            isSetOnCard = True
            set_retry_count = 0
            if isSendDone:
                print("done")
                mfrc = MFRC522.MFRC522()
                time.sleep(1)
                continue
            print ("Card detected")
        else:
            print("no card")
            time.sleep(1)
            # カードを外してないけど、読み取れないことがある(まぁこの辺は値段相応か？無線モノだしなぁ)
            if isSetOnCard and set_retry_count < 2:
                set_retry_count += 1
                mfrc = MFRC522.MFRC522()
                continue
            set_retry_count = 0
            isSendDone = False
            if isSetOnCard:
                send_data_at_remove_card()
            isSetOnCard = False
            mfrc = MFRC522.MFRC522()
            continue
        # Get the UID of the card
        (status,uid) = mfrc.MFRC522_Anticoll()
        # If we have the UID, continue
        if status == mfrc.MI_OK:
            print ("Card UID: "+ str(map(hex,uid)))
            # Select the scanned tag
            if mfrc.MFRC522_SelectTag(uid) == 0:
                print ("MFRC522_SelectTag Failed!")
            result = {}
            is_read_ok = True
            for sector in SECTOR_MAP:
                count = 0
                while True:
                    text_data = read_addr_for_order(mfrc, uid, sector)
                    # カードの読み込みでエラーが出ることがあるので、リトライ入れとく
                    if text_data == 0:
                        is_read_ok = False
                    break
                if not is_read_ok:
                    break
                result.update(text_data)
            if not is_read_ok:
                # カード読み直す。最初からやり直し
                mfrc = MFRC522.MFRC522()
                continue
            print("read end")
            send_data_at_put_card(result)
            isSendDone = True
            mfrc = MFRC522.MFRC522()



def destroy():
    GPIO.cleanup()

if __name__ == "__main__":
    try:
        loop()
    except KeyboardInterrupt:  # Ctrl+C captured, exit
        destroy()

```

### 5. いざ動作確認→トラブル
まぁそんなもんですよね。

###### １．GPIO動かない問題
まず、ラズパイ5では、GPIO.setupが動きませんでした。
上記コードを実行すると
RuntimeError: Cannot determine SOC peripheral base address
というエラーが出ました。
ググってみると、ラズパイ5では[GPIOは使えなくなったという情報](https://www.denshi.club/parts/2024/02/202310-bookworm-9-5gpio.html "ラズパイ2023年10月更新 bookworm ⑨ ラズパイ5のGPIO")がありました。
回避策もこちらのページにありました。
>回避策は、rpi-lgpioライブラリをインストールするようです。
>pip install --break-system-packages rpi-lgpio

「--break-system-packages」
初めて見ました。なんか恐ろしそうな響きのオプションなので、一旦調べることにします。
[こちらも情報](https://blog.jp.square-enix.com/iteng-blog/posts/00043-play-with-the-pep668/ "俺流！PEP668とうまくやっていく方法")がありました。
PEP668にて提案された仕様が関係しているそうです。
PEP668については、こちらも記事の引用から。
>内容としては、 OSが用意するパッケージのコマンド(dpkg/apt/dnf等)の管理外で、
>python用のモジュール管理コマンド（例：pip3等）を不用意に使うとOS側のpythonの環境に悪影響を及ぼしてしまうのを防ぎたいというものです。


OS側に影響の出るような変更は一旦エラーにするから、入れたい場合にはvenvで個別の仮想環境立てるか、
「--break-system-packages」使って影響を承知の上で入れるかしろってことみたいですね。
Node.jsで言うところのnpm installのインストール先をグローバルとローカルで分けて扱うのと同じイメージですかね。(そういう意味なら、当たり前な気がしてきた)

オプションの意味が分かったところでエラーの話に戻りまして、
GPIOモジュールを使うためには、rpi-lgpioライブラリのインストールが必要で、
こいつはOS側に変更が入るライブラリだから「--break-system-packages」が無いとインストール出来ない、ってことなんですね。

今回はPEP668について解説記事を書いてくださったスクエニITエンジニア様の対策その１を採用させていただき、
「納期に余裕がないから一番早い方法をタノム！」と叫びながら
「--break-system-packages」で強行突破しました。



###### ２．品質不安定問題
タグを置いてるのに「タグがない」って判定になったり、
セクターの読み取りの途中でエラーが出たり、
とにかく安定性に欠ける挙動をしました。
このタグとリーダーの個体の問題なのか、製品としての品質レベルがそうなのか、
そもそもRFIDってそういうものなのか、私には分かりませんでしたが、
手元にあるのはこのタグとリーダーなのでそういうものだと受け止めることにしました。

ですので、至る所でリトライをするように組んでます。
一応これで途中で落ちることは無くなりました。
(リトライ入れたけどやっぱいらなくて変な残骸になってる箇所ありますね。。。)


### 6. 動作風景
動画を撮ったので、そこから画像を切り貼りします。

タグのセット前です。画面項目は全てブランク状態です。
![rfid5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/1902b241-0e85-6b68-7cac-0a3c1dc15ab9.png)

置きます。カードから読み取ってデータをWebアプリ側に送信しています。(HTTPで送信)
![rfid6a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/9bd3c000-1a6e-78b3-f7b1-5fcb1dedf22d.png)

Webアプリが受信データから社員番号を取得し、
社員番号から名前と画像とその日の出退勤時刻を表示します。(WebSocketでブラウザに配信)
![rfid7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/dd613fbc-d649-2b13-cdf8-29871517be79.png)

出勤ボタンを押すと出勤時刻が入ります。
このディスプレイはタッチパネルなので、カードを置いたら画面をポチッとできます。
![rfid8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/6dbb2cd9-9f9d-42dd-9177-b76126081dc8.png)

退勤ボタンなら退勤時刻が入ります。
現在時刻表示の実装が適当過ぎて未来の時刻が入ってますね。
![rfid11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/e4436507-acf7-7cd9-b1ed-849e52bd9f5e.png)

カードを外すと
![rfid9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/5daf5789-7f6f-da45-9587-467fa7804c5b.png)
画面がブランクに戻ります。
![rfid10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/5074994d-7a19-7095-c629-b9035bad1524.png)

###### データの見え方
Beaconの章に書いたガントチャートでも出退勤時刻は出ますが、こんな感じでも見えます。
今日はどこで働いてて、いつからいつまで働いてて、どのくらい席に居たのか、ですね。
![sc_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935919/c1c490d0-7950-4004-1460-3490b1fc641e.png)
(この画像は出退勤も適当だし、Beaconの電源切り忘れてたせいで意味不明な勤務時間になってるし、ボロボロです。
稼働時間1,331分て、1日は1,440分しか無いのに。。。)

# まとめ
~~おもちゃで遊ぶのも~~IoT技術を学ぶのも記事を書くのも楽しかったです。
BeaconもRFIDのどちらにも特徴が有って、その特徴を掴んだ上でシステム設計をしないと
上手くいかない、そんな実感をすることが出来ました。
逆に上手に扱えれば、今まで集められなかった現場のデータが収集出来て、
現場の可視化・改善に繋がり、SEとして有意義な仕事が出来るようになると思います。
今後も楽しみながら学んでいきたいなと思います。

最後までお読みいただきありがとうございました。

<br>
<br>
<br>
なんか最初に自分の勤務を分析するとか改善するとか書いたんですが、<br>
この記事を書いた時点では何か出来るほどまともなデータは集まってません。<br>
よって、これから集めます！<br>

**俺たちの戦いはこれからだ！**