6
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonで学生証を読ませる(felica, nfcpy)

Last updated at Posted at 2022-07-26

はじめに...

課題でfelicaが組み込まれた学生証をタッチすることで入退室の管理システムをPythonで作ることになり、その備忘録的なもので残します...
最終的にはRaspberry piで実行することになりますが、デバッグの関係でMac環境で進めていますが、動作自体は同じなので触れません。

下準備

nfcpyインストール

nfcのライブラリ入れときます。
pip install nfcpy
Mac環境ではこれだけでおしまいなのですが、ラズパイではこのほかにやることがあります。

ラズパイでlsusbを実行して、リーダーを確認します。
今回使うリーダーはRC-S380通称"パソリ"なので
Sony Corporation RC-S380/P
ここのID xxx:yyyは後で要るのでメモっておいた方がいいです。(後々のために)

次に、cd /etc/udev/rules.d/にアクセスしてtouch nfcdev.rulesを作ってその中に以下のものを保存します。なおxxxyyyは上記で出したものを入れてください。自分は、入力ミスで引っかかってラズパイではしばらく動かすことができなかったなんて言えない
SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="xxx", ATTRS{idProduct}=="yyy", GROUP="plugdev"
これでラズパイでも動かすことができるようになりました。

FeliCaをとりあえず読む

ひとまずFeliCaを1秒間隔で、IDm PMm システムコードを出力するものをやってみました。

import nfc
import time

while True:
  with nfc.ContactlessFrontend('usb') as m: #felicaリーダーusb扱い
    tag = m.connect(rdwr={'on-connect': lambda tag: False}) #felicaカード情報読み込み
    print(tag) #情報出力
    time.sleep(1) #インターバル

IDm, PMmはカード固有に割り当てられた番号であり、この番号を使えば簡単に実現できるね!
...でおしまいにするのはなんとも楽すぎます。また、学生証をタッチする機会ではいずれも自分の学籍番号が出力されるが、このIDmをサーバーへ送って判定させるには早すぎるんじゃない?と思い、もしかしたらカードにリクエストを投げれば学番を出すことができるのでは?と思い方々調べて見ると、dumpすると学生証の中のデータを出力できるらしいということがわかったのでやってみました。

import nfc
import time

def connected(tag):
  # 内容を16進数で出力する
  print("dump felica.")
  print('  ' + '\n  '.join(tag.dump()))

while True:
  with nfc.ContactlessFrontend('usb') as m: #felicaリーダーusb扱い
    tag = m.connect(rdwr={'on-connect': connected}) #felicaカード情報読み込み
    print(tag) #情報出力
    time.sleep(1) #インターバル

出力するとこうなります(学番の部分などモザイクつけてます)。
スクリーンショット 2022-07-06 15.33.48のコピー.png
今回、自分が求めたい学籍番号単体で保存されているのがService 128:...(0x2008 0x200B)の部分です。
ただ、これを取り出すことが途中までできず、そのほかに保存されているService 106:...(0x1A88 0x1A8B)0001:~(省略)~|xxxx....|に他のデータとともに保存されており、そこからはすぐに取れそうだったので、それをやってみました。

学番取り出す 1

import nfc
   
service_code = 0x1a8b
   
def connected(tag):
  # タグのIDなどを出力する
  print(tag)
   
  if isinstance(tag, nfc.tag.tt3.Type3Tag):
    try:
      # 内容を16進数で出力する
      print("dump felica.")
      print('  ' + '\n  '.join(tag.dump()))
      # サービス/ブロックコード指定
      sc = nfc.tag.tt3.ServiceCode(service_code >> 6, service_code & 0x3f)
      # print("sc: "+ str(sc))
      bc = nfc.tag.tt3.BlockCode(0,service=0)
      # print("bc: "+str(bc))
      # 学籍番号出力
      feli_data = tag.read_without_encryption([sc],[bc])
      # print(str(feli_data[x:y]))
      print("gakuban:" + ''.join(str(feli_data[x:y])))
    except Exception as e:
      print("error: %s" % e)
  else:
    print("error: tag isn't Type3Tag")
   
# タッチ時のハンドラを設定して待機する
clf = nfc.ContactlessFrontend('usb')
clf.connect(rdwr={'on-connect': connected})

上記のソースコードでは、Service 106:...(0x1A88 0x1A8B)のところの0x1A8Bというサービスコードをまず指定してあげ、それをsc =...のところで調整してやってます。ここには学番以外のものも含まれているので、取り出したデータをfeli_data[x:y]とすることで、x, yはそれぞれdumpで出したデータの何番目かを決めてあげます。

まあ、これで出力できても結局誤魔化してる感満載なのと、他の人の学生証で試したところ、若干dumpデータに変化があり、これで出力し続けられない可能性も出てきたので、学番単体のところから出せないかまた試してみました。

学番を取り出す 2

いろいろ調べてみた結果、前述のサービスコード等とは別に、dumpしたデータにあるSystem 809Eというシステムコードも指定してあげる必要があるようで、それらをやってみると以下の通り

import nfc
import time

service_code = 0x200b

def connected(tag):
  # 内容を16進数で出力する
  print("dump felica.")
  print('  ' + '\n  '.join(tag.dump()))
  #システムコード指定
  idm, pmm = tag.polling(system_code=0x809E)
  tag.idm, tag.pmm, tag.sys = idm, pmm, 0x809E
  global gakuban

  if isinstance(tag, nfc.tag.tt3.Type3Tag):
    try:
      
      # 学籍番号出力
      sc = nfc.tag.tt3.ServiceCode(service_code >> 6, service_code & 0x3f)
      print("sc: "+ str(sc))
      bc = nfc.tag.tt3.BlockCode(0,service=0)
      print("bc: "+str(bc))
      feli_data = tag.read_without_encryption([sc],[bc])
      print(feli_data[0:8])
      gakuban = feli_data[0:8].decode()
      # def側学番出力
      print("gakuban:" + gakuban)
    except Exception as e:
      print("error: %s" % e)
  else:
    print("error: tag isn't Type3Tag")


while True:
  with nfc.ContactlessFrontend('usb') as m:
    tag = m.connect(rdwr={'on-connect': connected})
    # ループ側学番出力
    print("main gakunow: "+ gakuban)
    time.sleep(1)

これで、見事に単体で出せた!
ただし、データ自体は単体に見えてもバイナリ上若干データを含んでるらしく、プログラム上でもお分かりの通り、切り取った上でデコードしてます。あと、dumpのプログラムを動かしてるとたまにダメになることもあるようで、その時はコメントアウトしてください(テキトーすぎ)。

最終的にできたやつ↓

その後に、ラズパイのGPIOなりIFTTTなりで入退室のアプリ的なの作ったのがこちら
https://github.com/youk720/feligaku_inout/blob/main/main.py

後書き

当初は、課題でこのプログラム作っていたが、ただそれで終わらすのがもったいなくてこの記事書いた次第でしたが、出力結果のスクショをそういえば撮り忘れていたことに後悔...
あと、拙い文章力なのはお許しを...

参考...

6
10
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
6
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?