NFCカードリーダーを使う
Simple Wiki Based Contents Management System
関心分野 >> NFCカードリーダーを使う

NFCカードリーダーを使う

最近、iPhoneを交換し、TouchIDからFaceIDになりました。FaceIDは結構便利な部分はありますが、MobileSUICAでの支払いや勤怠管理システムを利用するときに、電源ボタンをダブルクリックする必要があり、面倒だなぁと思える部分もありました。
NFCカードリーダを使った勤怠管理システムでも、駅の改札のようにタッチのみでできないかと思い、色々試してみました。

使用する機器

ここで使用する機器は、ちょうど手元にあったRasPi3BとSony RS-S380で試していきたいと思います。
  • Raspberry Pi 3B(Raspberry Pi OS)
  • NFC Card Reader(Sony RC-S380)

RaspberryPiのセットアップ

RaspberryPiで動作するLinuxディストリビューションは、いくつかありますが、ここでは簡単に使用するために RaspberryPiOS(旧Raspbian)にしておきます。RaspberryPiOSは、RaspberryPi Imagerを使うと比較的簡単にインストールすることができます。
オフィシャルサイトからRaspberryPi Imagerをダウンロードして、OSをインストールしましょう。今回は、GUI環境は特に必要ありませんので、RaspberryPi OS Liteをインストールしています。

nfcpyのインストール

RaspberryPi OSのインストールが終了すれば、次に、RC-S-380を使用するために、nfcpyをインストールしていきます。現在のLinuxディストリビューションでは、Python2.7のオフィシャルサポートが切れていることもあり、nfcpyもPython3用のものしか公開されていません(正確には、Python2でもインストールできそうですが、pipでインストールできませんでしたのでスキップします)。
したがって、python3とpython3-pipをインストールしていきます。
sudo apt install python3 python3-pip
Python3とpipのインストールが終われば、nfcpyをpip3コマンドを使ってインストールします。
sudo pip3 install nfcpy
以上で、ソフトウェアの準備は完了です。

NFCカードリーダの接続確認

ソフトウェアの準備が整えば、RC-S380をRaspberryPiに接続して、認識しているかどうかを確認します。NFCカードリーダの接続確認には、lsusbコマンドを使用し下のように入力します。
$ lsusb | grep S380
Bus 001 Device  010: ID 054c:06c3 Sony Corp. RC-S380
今回使用するNFCカードリーダの名称または、Sonyでgrepすれば、接続されているかを確認できます。

nfcpyのセットアップ

nfcpyを使ってNFCカードリーダを使用できるかどうかの確認は、下のコマンドを実行してみます。
$ python3 -m nfc
このコマンドを入力すると、接続されているNFCカードリードを検索してユーザモードでアクセスできるかを確認することができます。また、パーミッションの設定などをどうすればいいかも、このコマンドを実行すると表示されますので、その通り実行すればOKです。
おそらく最初に実行した場合には、下のようになると思います。
$ python3 -m nfc
This is the 1.0.3 version of nfcpy run in Python 3.7.3
on Linux-5.10.52-v7+-armv7l-with-debian-10.10
I'm now searching your system for contactless devices
** found usb:054c:06c3 at usb:001:007 but access is denied
-- the device is owned by 'root' but you are 'pi'
-- also members of the 'root' group would be permitted
-- you could use 'sudo' but this is not recommended
-- better assign the device to the 'plugdev' group
   sudo sh -c 'echo SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="06c3", GROUP="plugdev" >> /etc/udev/rules.d/nfcdev.rules'
   sudo udevadm control -R # then re-attach device
I'm not trying serial devices because you haven't told me
-- add the option '--search-tty' to have me looking
-- but beware that this may break other serial devs
Sorry, but I couldn't find any contactless device
このメッセージに従って、下のように入力して再起動します。
$ sudo sh -c 'echo SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="06c3", GROUP="plugdev" >> /etc/udev/rules.d/nfcdev.rules'
$ sudo udevadm control -R
$ sudo reboot
再起動後、再度接続確認を行うと下のようになります。
$ python3 -m nfc
This is the 1.0.3 version of nfcpy run in Python 3.7.3
on Linux-5.10.52-v7+-armv7l-with-debian-10.10
I'm now searching your system for contactless devices
** found SONY RC-S380/P NFC Port-100 v1.11 at usb:001:010
I'm not trying serial devices because you haven't told me
-- add the option '--search-tty' to have me looking
-- but beware that this may break other serial devs
これでnfcpyのセットアップは完了です。

NFCカードを読み取る

nfcpyのセットアップが完了しましたので、早速、NFCカードの読込をやっています。Python3のインタプリタを起動します。
$ python3
Python 3.7.3 (default, Jan 22 2021, 20:04:44)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
次に、nfcパッケージを読込RC-S380との接続を確立します。
 >>> import nfc
 >>> clf = nfc.ContactlessFrontend('usb')
次に、clf.connectメソッドを使って、iPhoneのSUICAを読み取ってみます。
 >>> tag=clf.connect(rdwr={'on-connect': lambda tag: False})
 >>> print(tag)
Type4ATag MIU=255 FWT=0.038664
iPhoneでは、clf.connectメソッドで特に指定しないとType4ATagとして読み取ってしまいます。本来ならば、Felicaカード(Type3Tag)で読み取ってほしいので、targetを指定してみます。
clf.connectメソッドでのtargetの指定は、targetsオプションで行うことができます。Type3Tagのみを読み込むようにするには、下のようにtargetsに '212F'を指定します。
 >>> tag=clf.connect(rdwr={'targets': ['212F'], 'on-connect': lambda tag: False})
 >>> print(tag)
 Type3Tag 'FeliCa Mobile 3.0' ID=XXXXXXXXXXX PMM=YYYYYYYYYY SYS=0003
これでiPhoneのSUICAのIDを取得することができました。この時、iPhoneでは、SUICAをエクスプレスカードに設定しているのですが、読み取るときにダブルクリックを要求されます。これだと何かと面倒なのでタッチのみで認識できるようにしていきます。

iPhoneでSUICAの読み取りを簡単にする

色々調べてみるとiPhoneでエクスプレスカードに設定したときに、タッチのみで情報にアクセスするためには、clf.connectメソッドで使用しているtargetsに対して、system_idをセットしてあればいいようです。交通系Felicaでは、system_idは "0003" ですので、これを指定してプログラムを作成してみます。
clf.connectメソッドでは、targetsというオプションは指定できるのですが、こればtargetのIDであり、nfc.clf.RemoteTargetを直接指定することはできません。そのため、'on-startup'のコールバック関数on_startupを定義してsystem_idをセットしてみます。
 >>> def on_startup(targets):
 ...  for target in targets:
 ...    target.sensf_req=bytearray.fromhex("0000030000")
 ...  return targets
 ...
 >>> tag=clf.connect(rdwr={'targets': ['212F'], 'on-startup': on_startup, 'on-connect': lambda tag: False})
 >>> print(tag)
 Type3Tag 'FeliCa Mobile 3.0' ID=XXXXXXXXXXX PMM=YYYYYYYYYY SYS=0003
このコードで実行すると、エクスプレスカードに設定したiPhoneのSUICAでは、ロック状態からでもタッチのみで認識できました。
NFCカードリーダで扱うFelicaが交通系カードであれば、これでもいいのですが、日本で使われているFelicaカードには、Edy、ExpressCard、nanacoなど他にも色々あります。上記の実装だと、他のFelicaカードを認識できませんので、何とかする必要があります。

他のFelicaカードも同時に使えるようにする

前述の設定だと、交通系カードであれば問題なく利用できるのですが、他のFelicaカードも同じように使えるようにしてみたいと思います。
clf.connectメソッドの実装を覗いてみると、大まかに下の処理が実行されていた。
  1. targetsオプションで指定されたRemoteTargetを生成
  2. on-startupで設定したコールバック関数を実行
  3. clf.sense関数でカードに対応したtargetを検出
  4. 検出したtargetからTagを導出
  5. on-connectで設定したコールバック関数を実行
この実装をみると、Felicaに対応するRemoteTargetを複数含めても良さそうである。したがって、これと同じような処理を実装したクラスを作成してみます。
class CardReader(object):
  def __init__(self):
    self.clf=nfc.ContactlessFrontend('usb')
    self.target = None

    self.T106A = nfc.clf.RemoteTarget("106A")
    self.T106B = nfc.clf.RemoteTarget("106B")
    # FeliCa
    self.suica = self.set_felica("0003")
    self.edy = self.set_felica("8054")
    self.express = self.set_felica("854C")
    self.nanaco = self.set_felica("04C7")
    self.waon = self.set_felica("684F")

    self.targets = [ self.suica, self.T106A, self.T106B, self.edy, self.express, self.nanaco, self.waon ]

  #
  #
  def set_felica(self, sys_id):
    target = nfc.clf.RemoteTarget("212F")
    target.sensf_req=bytearray.fromhex("00%s0000" % sys_id)
    return target

  #
  #
  def sense(self):
    self.target = self.clf.sense(*self.targets, interval=0.2)
    return self.target

  #
  #
  def on_discover(self, target):
    if target is None: return False
    if target.sel_res and target.sel_res[0] & 0x40:
      return False
    elif target.sensf_res and target.sensf_res[1:3] == b"\x01\xFE" :
      return False
    else:
      return True   

  #
  #
  def get_idm(self, tag):
    if bool(tag):
      return binascii.hexlify(tag.identifier).decode()
    return None
 
  #
  #
  def read_card_id(self):
    while self.sense() is None: time.sleep(0.1)
    if self.on_discover(self.target) :
      tag = nfc.tag.activate(self.clf, self.target)
      return tag, self.get_idm(tag)

このクラスでは、Felicaに対応するRemoteTargetにSYSTEM IDを明示的に設定し、それらを複数用意することでFelicaカードの待ち受けをしています。現在、よく使われている。交通系カード、Edy、nanaco、waon、ExpressCardには対応していますが、QuickPayのカードや他のブランクカードには対応していません。SystemIDを設定しないRemoteTaegetを追加すると、すべてのカードには反応しますが、iPhoneのエクスプレスカードにしていしたものでも、ダブルクリックをしないと読み取れないようになります。

nfc_readerでIDmを取得する

iPhoneでエクスプレスカードに設定したときに、タッチのみでIDを取得するようなクラスを実装してみました。Githubに公開していますので、ダウンロードして動作確認をしてみてください。
 $ git clone https://github.com/haraisao/nfc_reader
 $ cd nfc_reader
 $ python3 scripts/nfc_reader.py
 Please touch
 <<ここでタッチ>>
 xxxxxxxxxxxxx Type3Tag 'FeliCa Mobile 3.0' ID=XXXXXXXXX PMM=YYYYYYYYYYYY SYS=FFFF