Help us understand the problem. What is going on with this article?

Raspberry Piを使って学生証から学籍番号を読み取ろう

導入

私が所属している大学の研究室では、元々先人の方が部屋の扉の鍵が開けた、閉じたというのをAmazon Dashボタンを押すことによってSlackに投げてシステムが構築されていたのですが、今年に入ってすぐ内部のボタンが寿命を迎えたのか、通知が飛ばなくなってしまいました。新しくボタンを新調する、電池を換えるなどの考えはあったのですが、どうせなら卒業研究でこれらの発展として部屋の扉だけでなく学生証を使って在室状況を確認できれば、電池切れなどでシステムが動かなくなるということは無くなりますし、さらに在室状況を確認することによって鍵の管理が楽になるのではと思い、友人と協力して卒業研究の一環で研究室内の在室状況を確認するシステムの内、「学生証から学籍番号を読み取る部分」のシステムを私が担当することになりました。その備忘録として記事に残させていただきます。…と言っても前例(静岡大学会津大学の例があるのでそれをベースとして構築させていただきました。)

なお私はPythonに関しては全くのド素人です。ゼミの一環でGPIOからLEDなりタクトスイッチなりでちょっと動かした程度。

環境

  • サーバ(今回ではRaspberry Pi)
  • Python3.8.1
  • nfcpy 1.0.3
  • SONY PaSoRi RC-S380

構築

今回サーバーとしてRaspberry Pi4を研究室側から支給させてもらったため、PythonでNFCカードリーダーをいろいろするためのライブラリを探してみたところ、どの記事もnfcpyを使っているため(ほかに選択肢が無い)nfcpyを使って色々動かすことにしました。またカードリーダーにはSONYのPaSoRiを使用する例がほとんどでしたので、研究するために自腹切って購入しました。

nfcpyの公式ドキュメント

某新型ウイルスによって開始時期が大幅にずれたため構築を始めたのは7月からです。その段階ではRaspberry Pi4を使っている例は無かったのですが結果を言うと成功しました。

ドライバ関連

  1. nfcpyを入れる

まずnfcpyを入れます。これが無いと始まらない。
※ちなみにサーバはパッケージの更新まで終わっていることを想定しています
sudo pip install --upgrade pip//pipの更新
sudo pip install nfcpy//nfcpyのインストール
pip show nfcpyでnfcpyが出ればインストール成功です。
2. PaSoRiを認識させる

こちらの記事を参考にいたしました
python -m nfcと入力した際にエラーをはいてくるはず。
以下のコマンドを打ちます(長い)
sudo sh -c 'echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules'
記事によるとport100が悪さをしているそう。
sudo udevadm control -R
sudo modprobe -r port100
sudo sh -c 'echo blacklist port100 >> /etc/modprobe.d/blacklist-nfc.conf'
これでport100が起動しなくなります。
python -m nfcを打ち込むとPaSoRiを認識できるようになります。

これでNFCカードリーダーを読み取るための準備ができました。

学生証のデータ読み取り

以上の準備が終わりましたら学生証を読み取るためのプログラムを書いていきます。

学生証に格納されているデータをダンプする

学生証を始めとしたNFCカードはデータが内部にそのまま格納されているわけでは無い為(当たり前)、目的のデータがどの領域にあるかを調べる必要があります。対象は違いますが、同じようにダンプを行ってくれるプログラムを書いていた先駆者様が居ましたので、こちらのコードを使って学生証をダンプしました。抽出したデータは以下の通りですが、筆者である私の個人情報に繋がりかねないため、今回の核である学籍番号部分以外は省かせてもらいます。

Type3Tag 'FeliCa Standard (RC-SA00/1)' ID=0116040086180701 PMM=033242828247AAFF SYS=809E
  System 809E (unknown)
  Area 0000--FFFE
    Area 1000--10FF
      Random Service 64: write with key & read w/o key (0x1008 0x100B)
      Area 2000--20FF
      Random Service 128: write with key & read w/o key (0x2008 0x200B)
       0000: XX XX XX XX 32 31 31 35 00 00 00 00 00 00 00 00 |*XXX2115........|
      Area 3000--30FF
      Random Service 192: write with key & read w/o key (0x3008 0x300B)
      Area 4000--40FF
      Random Service 256: write with key & read w/o key (0x4008 0x400B)
  System FE00 (Common Area)
  Area 0000--FFFE
    Area 1A80--1AFF
      Area 1A81--1AFF
        Random Service 106: write with key & read w/o key (0x1A88 0x1A8B)

以上の結果を見ると、弊学ではシステムコード:0x809Eのサービスコード0x200Bにアスキーコードとして格納していることが判明しました(学籍番号をそのまま出すと身バレに繋がるため最初の4文字分は伏せております)ので、NFCカードを読み取った際にそれらのブロックコードを読みだして抽出してあげれば学籍番号を取得することができます。

NFCを読み取る

以下nfcpyを使って学籍番号を読み取ったプログラムです。

read_nfc.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import binascii
import nfc

#弊学の学生証の学籍番号が格納されているサービスコード
service_code = 0x200B

def connected(tag):

    if isinstance(tag, nfc.tag.tt3.Type3Tag):
        try:
            #16ビットのservice_codeからservice >> 6で上位10ビットを取り出し、service_code && 0x3fで下位6ビットを取り出す
            svcd = nfc.tag.tt3.ServiceCode(service_code >> 6, service_code & 0x3f)
            #serviceはread_without_encryptionの引数service_list内でのインデックス
            blcd = nfc.tag.tt3.BlockCode(0,service=0)
            #read_without_encryptionでタグの指定した部分の情報内のブロックデータを読み取る
            block_data = tag.read_without_encryption([svcd], [blcd])
            #今回ではブロックデータの1文字目から8文字目に格納されている、それを文字列に変換しutf-8でデコード
            student_id = str(block_data[0:8].decode("utf-8"))
            print(student_id)
        except Exception as e:
            print("Error:%s" % e)
    else:
        print("Error:tag isn't Type3Tag")

    #値をTrueで返すと触れて離すまでの間、一回だけ処理を行う
    return True

clf = nfc.ContactlessFrontend('usb')

def main():
    while True:
        #学生証を読み取るまで待機
        clf.connect(rdwr={'on-connect': connected,})

try:
    main()
except KeyboardInterrupt:
    print("Forced termination")
    clf.close()
    sys.exit(0)

正直スキル不足で解説らしいことがあまりできないのですが、備忘録なので個人的なメモとして。参考

まず最初にclf=nfc.ContactlessFrontend('usb')はUSB接続のNFCリーダーを開くという意味で、これはPaSoRiを使うよっていう宣言程度に捉えています。
clf.connectで実際にNFCカードリーダーを起動し、NFCタッチがされるまで待機します。connectの引数の内、起動時であるon-startup、タッチしたときのon-connect、カードを離したときのon-releaseなどのコールバックがあります。それぞれにコールバック関数を指定することでアクションによって挙動を変化させることができます。今回はon-connectに関数connected(tag)を指定します。
リーダーにカードをタッチしたときに呼び出すconnectedの中には、そのタグのオブジェクトを表すtagが渡されます。以降はそのtagを使ってNFCタグと通信を行います。

NFCのサービスコード自体は 16 ビットの値で、上位10ビットがサービス番号、下位6ビットが属性値です。今回の場合なら、調べるサービスコードは取得済みですので(0x200B)、正直変数に入れなくても動く可能性が高く、余計なメモリを取らないで済みそうですが、複数のサービスコードを使う場合においては配列を使って調べたほうが効率は良くなると思います。
nfc.tag.tt3.ServiceCode(service_code >> 6, service_code & 0x3f)は、16ビットの整数serviceからServiceCodeオブジェクトを生成するやり方です。service_code >> 6 で上位 10 ビットを取り出します。service_code & 0x3fで下位6ビットを取り出します。

次にデータブロックを読み込んでいくのですが、read_without_encryptionのドキュメントによると、Blockcodeの生成時に渡すservice=0はサービスコードそのものではなくread_without_encryptionの引数の引数service_list内でのインデックスです。1サービスにつき16バイトの大きさのブロックを持ち、サービス内で0から連番が割り振られています。今回の場合では0番目から7番目に入っているという訳なのですが、ここでstudent_id = str(block_data[0:8].decode("utf-8"))とすると最後のデータ分はぶった切られるため注意が必要です。文字コードに関してはシステムで使うOSにもよると思いますが、弊学の学生証はShift_JIS(滅びてほしい)で、Raspberry OSを始めとしたLinuxのシステムには不都合が多すぎるため、基本的にutf-8にデコードをしておくのが無難です。

最後にconnectedの返り値をTrueにすると、カードを触れて離すまでの間で1回だけ処理を行うようになります。逆にFalseだと何度も読み取る処理が発生するため、今回のシステム上不都合になるためちゃんとTrueにしておきましょう。

最後に

今回ほとんどPython知らない状態からのスタートでしたので、出典の記事を大いに参考にしてプログラムを書いたのですが、意外と何とか動くものにはなるんですね。まだ試作品段階ですので、いろいろと改良をして卒業研究として発表できるようにしていきます。

追記

(2020/10/16 19:36)
ブロックデータの隠し忘れ箇所があるとご報告をいただきましたので反映しました。

Requin
にわか知識でジャンクと戯れる人です
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away