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

### はじめに...
課題で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`を作ってその中に以下のものを保存します。なお`xxx`と`yyy`は上記で出したものを入れてください。~~自分は、入力ミスで引っかかってラズパイではしばらく動かすことができなかったなんて言えない~~
```SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="xxx", ATTRS{idProduct}=="yyy", GROUP="plugdev"```
これでラズパイでも動かすことができるようになりました。

## FeliCaをとりあえず読む
ひとまずFeliCaを1秒間隔で、`IDm` `PMm` `システムコード`を出力するものをやってみました。
```Python
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すると学生証の中のデータを出力できるらしいということがわかったのでやってみました。
```Python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185343/bbaa0286-3536-f94d-6c72-fa7819078145.png)
今回、自分が求めたい学籍番号単体で保存されているのが`Service 128:...(0x2008 0x200B)`の部分です。
ただ、これを取り出すことが途中までできず、そのほかに保存されている`Service 106:...(0x1A88 0x1A8B)`の`0001:~(省略)~|xxxx....|`に他のデータとともに保存されており、そこからはすぐに取れそうだったので、それをやってみました。

## 学番取り出す 1
```Python3
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`というシステムコードも指定してあげる必要があるようで、それらをやってみると以下の通り
```Python
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

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

## 参考...
* https://qiita.com/zaburo/items/db10ceadd49568d6f6f1
* https://aizu-vr.hatenablog.com/entry/2019/08/02/nfcpy%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%AD%A6%E7%94%9F%E8%A8%BC%E3%81%8B%E3%82%89%E5%AD%A6%E7%B1%8D%E7%95%AA%E5%8F%B7%E3%82%92%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8B
* https://qiita.com/koinori/items/ee81e1815de9e4d7f15d