導入
私が所属している大学の研究室では、元々先人の方が部屋の扉の鍵が開けた、閉じたというのを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を使用する例がほとんどでしたので、研究するために自腹切って購入しました。
某新型ウイルスによって開始時期が大幅にずれたため、構築を始めたのは7月からです。その段階ではRaspberry Pi4を使っている例は無かったのですが結果を言うと成功しました。
ドライバ関連
nfcpyを入れる
まずnfcpyを入れます。これが無いと始まらない。
※ちなみにサーバはパッケージの更新まで終わっていることを想定しています
sudo pip install --upgrade pip
//pipの更新
sudo pip install nfcpy
//nfcpyのインストール
pip show nfcpy
でnfcpyが出ればインストール成功です。
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が悪さをしているそうなので、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)'
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を使って学籍番号を読み取ったプログラムです。
#!/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:
svcd = nfc.tag.tt3.ServiceCode(service_code >> 6, service_code & 0x3f)
blcd = nfc.tag.tt3.BlockCode(0,service=0)
block_data = tag.read_without_encryption([svcd], [blcd])
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-start:起動時したときに呼ばれる
on-connect: タッチしたとき
on-release:カードを離したときに呼ばれる
ここに指定しているアクションによって挙動を変化させることができます。今回はon-connectを引数で指定します。
リーダーにカードをタッチしたときに呼び出す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)
ブロックデータの隠し忘れ箇所があるとご報告をいただきましたので反映しました。
(2023/10/19)
社会人になってこの記事を読み直したら、クッソ読みづらかったので少しだけ加筆修正しました。