💳

PHP で NFC リーダーを実装する

2021/10/11に公開

みなさん、こんにちは!めもりー(@m3m0r7) です。
PHP に FFI が導入されてから、だいぶ時が経ちましたが、使いみちが特に思い浮かばず…

そして、ふと「あれ PHP で NFC リーダー実装できるのでは」と思い至り、実際に実装してみました。

TL;DR

前提・必要なもの

まず、NFC リーダーの実装にあたって、重要なのはデバイスです。そして、実際に実装できるのかどうかを先駆者が居ないかインターネットで調べるということです。

先駆者自体は居て、Python での実装例をいくつかに参考にしました。

PHP での先駆者は居なさそう…?

そして なるほど、PaSoRi Sony RC-S380 というデバイスを買えばできるんだな と雑に所感を得て、酔った勢いでポチりました。

これが、間違いだと気づいたのは届いてから数時間後のことです…。後述する libnfc が対応しているのは RC-S330 というデバイスで RC-S380 は非対応のようです。
Python のライブラリである nfcpy は、RC-S380 のための独自実装[1]を行っています。
ただ PHP を使ってノリで遊ぶにはだいぶハードルが高いなと思い RC-S330 を買い直しました。

次に NFC を扱うにはどうすればいいのか、というのを調べて見た結果いくつか方法はあるようでした。

  • libusb[2] を使って愚直に一から実装する
  • libnfc[3] を使ってある程度形付けられたもので実装する

私は、とりあえず PASMO タッチして遊びたい!!!!という欲求のほうが強かったので後者の libnfc という選択を取りました。

どう実装するか

実際にどう実装するかですが PHP の FFI を用いて実装できます。 FFI::cdef の cdef に C で実装されたヘッダーファイルの中身を渡して上げると、制限付きではあるものの、指定されたライブラリの機能を使用することできます。

例えば以下のように使用します。

$ffi = FFI::def(<<<_
int printf(const char *format, ...);
_, 'libc.so.6'); 

$ffi->printf("Hello %s!\n", "world")

PHP の FFI が便利なのは char *int のようなスカラーで返ってくる値は PHP の型に変換してくれるという点です。

また、内部でポインタが必要な場合 FFI::addr を使ってあげることでパラメータにポインタを渡すことも可能です。

これらを駆使して libnfc の機能を使いつつ実装していきます。

事前準備

libnfc を PHP で使うにあたって、いくつか事前準備が必要になります。

libnfc の導入

Mac の場合、以下で入れられます

brew install libnfc

執筆時点で 1.8.0 なので libnfc のライブラリのパスは恐らく /usr/local/Cellar/libnfc/1.8.0/lib/libnfc.dylib になるんじゃないかと思われます。

NFC リーダーの購入

RC-S330 を買いましょう!

libnfc のサンプルコードを参考に

サンプルコードがないと何も始まらないので、サンプルコードをいくつか探した結果、公式のサンプルがわかりやすかったので、参考にしつつ実装をしていきます。

上記のサンプルコードを参考に実装をしていきます。まず必要なヘッダーファイルですが nfc-internal.hnfc-types.h, nfc.h になります。

これらをいい感じに 1 枚岩にして FFI 用に整形します。どうやるかは以下のコードを見てもらえればと思います。

インストールした libnfc のサンプルを動かしてみます。

まず接続されているデバイスの一覧を取得してみます。

$ /usr/local/Cellar/libnfc/1.8.0/bin/nfc-list 

/usr/local/Cellar/libnfc/1.8.0/bin/nfc-list uses libnfc 1.8.0
nfc-list: ERROR: Unable to open NFC device: pcsc:Yubico YubiKey OTP+FIDO+CCID
NFC device: Sony /  opened

どうやら、エラーは出ていますが、なんかそれっぽい値は出力できていそうです。

次に FeliCa をタッチしたときの情報を出してみます。

$ /usr/local/Cellar/libnfc/1.8.0/bin/nfc-poll

/usr/local/Cellar/libnfc/1.8.0/bin/nfc-poll uses libnfc 1.8.0
NFC reader: Sony /  opened
NFC device will poll during 36000 ms (20 pollings of 300 ms for 6 modulations)
FeliCa (212 kbps) target:
        ID (NFCID2): 01  2e  48  c2  3c  96  28  95
    Parameter (PAD): 00  f1  00  00  00  01  43  00
   System Code (SC): 88  b4
Waiting for card removing...error       libnfc.driver.pn53x_usb Application level error detected
error   libnfc.driver.pn53x_usb Application level error detected
error   libnfc.driver.pn53x_usb Application level error detected
nfc_initiator_target_is_present: Target Released
done.

めちゃくちゃエラーは出ていますが、それっぽい出力が垣間見えています。
なお YubiKey など他のデバイスが刺さっている場合、そちらの方を見てしまう可能性があるので、予め認識させたいデバイス以外は抜いておくことをオススメします。

PHP での実装についてですが、どうやらデバイスに FeliCa などがタッチされた時に何かしら発火させる処理は nfc-poll.c に書かれているようなので、これを PHP に翻訳してみます。

$libraryPath = '/path/to/libnfc.dylib';
$ffi = FFI::cdef(<<<_
// ... 省略
_, $libraryPath);

$context = $ffi->new('nfc_context *');
$ffi->nfc_init(FFI::addr($context));

$pnd = $ffi->nfc_open($context, null);

if ($pnd === null) {
    echo "Unable to open NFC device\n";

    $ffi->nfc_exit($context);

    exit(1);
}


if ($ffi->nfc_initiator_init($pnd) < 0) {

    $ffi->nfc_perror($pnd, "nfc_initiator_init");

    $ffi->nfc_exit($context);

    exit(1);
}

printf("NFC reader: %s opened\n", $ffi->nfc_device_get_name($pnd));

$nt = $ffi->new('nfc_target');

// nfc_modulation_type nmt;
//      nfc_baud_rate nbr;
$nmmMifare = $ffi->new('nfc_modulation[6]');

$nmmMifare[0]->nmt = $ffi->NMT_ISO14443A;
$nmmMifare[0]->nbr = $ffi->NBR_106;

$nmmMifare[1]->nmt = $ffi->NMT_ISO14443B;
$nmmMifare[1]->nbr = $ffi->NBR_106;

$nmmMifare[2]->nmt = $ffi->NMT_FELICA;
$nmmMifare[2]->nbr = $ffi->NBR_212;

$nmmMifare[3]->nmt = $ffi->NMT_FELICA;
$nmmMifare[3]->nbr = $ffi->NBR_424;

$nmmMifare[4]->nmt = $ffi->NMT_JEWEL;
$nmmMifare[4]->nbr = $ffi->NBR_106;

$nmmMifare[5]->nmt = $ffi->NMT_ISO14443BICLASS;
$nmmMifare[5]->nbr = $ffi->NBR_106;

$szModulations = 6;
$uiPollNr = 20;
$uiPeriod = 2;

if ($ffi->nfc_initiator_poll_target($pnd, $nmmMifare, $szModulations, $uiPollNr, $uiPeriod, FFI::addr($nt)) > 0) {
    $string = $ffi->new('char *');
    $ffi->str_nfc_target(FFI::addr($string), FFI::addr($nt), true);

    echo FFI::string($string) . "\n";
}

$ffi->nfc_close($pnd);

$ffi->nfc_exit($context);

上記を書いた後、実際に FeliCa をカードリーダーにかざしてみます。

image

※この PASMO は私が通勤という苦難を乗り越えていたときの証です。

そうすると、以下のような出力が取得できます。

NFC reader: Sony /  opened
FeliCa (212 kbps) target:
     ID (NFCID2): XX  XX  XX  XX  XX  XX  XX  XX
 Parameter (PAD): YY  YY  YY  YY  YY  YY  YY  YY
System Code (SC): ZZ  ZZ

以上が PHP で NFC リーダーを実装する例でした。

不具合

Mac 上でデバイスを使うと Application level error detected が出てきたり、1回 FeliCa をかざした後に再度サンプルの /usr/local/Cellar/libnfc/1.8.0/bin/nfc-poll を実行すると、上記のエラーが出力されて、最終的に認識されなくなったりします。

ちなみに、これは libnfc を brew からのインストールではなく Mac 上でビルドしたものを使っても同一の結果でした。

しかし、解決方法が分からず…。ただ、少なくても期待していない結果ではあるようなので、Raspberry Pi ではどうなのか、実際に試そうかなと思い、ポチりました。これについてはいずれ、また…。

最後に

不具合をいくつかアプリケーション側で対応し、清書したものは https://github.com/m3m0r7/nfc-for-php です。より手軽に、実装したい方どうぞ!

脚注
  1. https://github.com/nfcpy/nfcpy/blob/master/src/nfc/clf/rcs380.py ↩︎

  2. https://github.com/libusb/libusb ↩︎

  3. https://github.com/nfc-tools/libnfc ↩︎

Discussion