音声強化版PaPeRo i とAndroidでNDEFデータ通信を行う(NFCリーダーライター)

 音声強化版PaPeRo i とAndroidでNDEFデータ通信を行う(NFCカードエミュレーション)の通信方向を逆にし、ソニー製カードリーダーライターPaSoRi RC-S380を接続した音声強化版PaPeRo i のTinker Board S をNFCリーダーとして動作させ、AndroidからカードエミュレーションでNDEFデータを送信する方法について調べました。

nfcpyでリーダーライター

 nfcpyはカードエミュレーションだけでなく当然リーダーライターの機能も持っています。音声強化版パペロのTinker Board S でもpip3コマンドで普通にインストールできます。

pip3 install nfcpy

リーダーライターモードでカードを読む方法については公式ページGetting startedのread-and-write-tagsという章で説明されていますが、最初に紹介されているclf.sense()はカードをポーリングする方法でタッチされたイベントは検知できないため、次に紹介されているclf.connect()を使用しました。これはカードエミュレーションで使用したのと同じメソッドですがcardパラメータではなくrdwrパラメータを指定することでリーダーライター動作になります。
読み取ったNFCカードにNDEFデータが存在すれば自動的にtag.ndefに格納される仕組みの様なのですがAndroidのカードエミュレーションとの組み合わせでは機能しませんでした。これはAndroidでType4カードをエミュレートする場合AIDという識別子を申請して取得するか、または自由に使える範囲(0xFで始まる値)のうち任意の値を決めて使用する必要があるのですが、これがnfcpyでは実カードの値にしか対応していないためと思われます。そのためnfcpyのインスタンスの該当するメソッドを動作中に上書きし、’_’で始まる内部メソッドを直接使用する方法で少々無理やりですが対応させました。

ndefcardreader.py

import signal
import sys
import time
from logging import (getLogger, basicConfig, Formatter,
                     DEBUG, INFO, WARN, ERROR, CRITICAL)

import ndef
import nfc
from nfc.clf import RemoteTarget

logger = getLogger(__name__)

class NdefCardReader:
    def __init__(self, dev='usb:054c:06c3', idm='03FEFFE011223344',
                 pmm='01E0000000FFFF00', sys='12FC', brty='212F',
                 open_retry_sec=10.0,
                 hex_aid = 'F1234567890ABC',
                 on_ndef_data=None,
                 on_ndef_err=None,
                 ):
        self.dev = dev
        self.idm = idm
        self.pmm = pmm
        self.sys = sys
        self.brty = brty
        self.open_retry_sec = open_retry_sec
        # Android側と一致させる必要がある
        self.aid = bytearray.fromhex(hex_aid)
        self.on_ndef_data = on_ndef_data
        self.on_ndef_err = on_ndef_err
        self.clf = None

    def on_startup(self, targets):
        logger.info('targets: {}'.format(targets))
        return targets

    def tag_found(self, tag):
        # tagが見つかった
        # NDEFインスタンスを利用したいので直接作って使う
        ndefobj = tag.NDEF(tag)
        if ndefobj is None:
            logger.info('NDEF not found.')
            return
        if tag.type == 'Type4Tag':
            ndefobj._aid = self.aid
            # _read_ndef_data()の実行中に呼ばれる
            # select_ndef_applicatoin()はaidの想定が
            # 本物のDESFireの値になっており変更できないので
            # ここでオーバーライドする
            def select_ndef_application():
                try:
                    ndefobj.tag.send_apdu(0, 0xA4, 0x04, 0x00, self.aid)
                    return True
                except:
                    return False
            ndefobj._select_ndef_application = select_ndef_application
        else:
            logger.error('not supported? type: {}'.format(tag.type))
        dat = ndefobj._read_ndef_data()
        if dat is not None:
            try:
                res = list(ndef.message_decoder(dat))
                logger.debug('NDEF Record list: {}'.format(res))
                if callable(self.on_ndef_data):
                    self.on_ndef_data(res)
            except Exception as e:
                logger.error(e)
                logger.info('non NDEF data: {}'.format(dat))
                if callable(self.on_ndef_err):
                    self.on_ndef_err((dat, e))

    def on_connect(self, tag):
        logger.info('tag: {}'.format(tag))
        if tag is None:
            return False
        else:
            self.tag_found(tag)
            return True

    def on_released(self, tag):
        logger.info('released: {}'.format(tag))
        return True

    def run(self):
        while True:
            try:
                with nfc.ContactlessFrontend(self.dev) as self.clf:
                    while True:
                        while self.clf.connect(rdwr={
                            'on-startup': self.on_startup,
                            'on-connect': self.on_connect,
                            'on-release': self.on_released,
                        }):
                            logger.info('in connect loop')
            except OSError as e:
                if e.errno == 19:
                    time.sleep(self.open_retry_sec)
                    logger.info('retry open.')
                else:
                    logger.error(e)
                    break
            except Exception as ex:
                logger.error(ex)
                break

        logger.info('end')

def _test_on_ndef_data(msg):
    logger.info('ndef record list={}'.format(msg))

def _test():
    reader = NdefCardReader(on_ndef_data=_test_on_ndef_data)
    reader.run()

if __name__ == '__main__':
    loglevel = INFO
    LOG_FMT = '%(asctime)s %(levelname)s %(thread)d.%(name)s.%(funcName)s %(message)s'
    basicConfig(level=loglevel, format=LOG_FMT)
    _test()

Androidでカードエミュレーション

 Android側のカードエミュレーション処理についてはhttps://github.com/TechBooster/C85-Android-4.4-Sample/tree/master/chapter08を参考にさせていただきました。https://github.com/TechBooster/C85-Android-4.4-SampleにはライセンスApache-2.0の表示があります。

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.ndefemu002">

    <uses-feature
        android:name="android.hardware.nfc.hce"
        android:required="true"
        tools:targetApi="eclair" />

    <uses-permission android:name="android.permission.NFC" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name=".NdefHostApduService"
            android:exported="true"
            android:permission="android.permission.BIND_NFC_SERVICE">
            <intent-filter>
                <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <meta-data
                android:name="android.nfc.cardemulation.host_apdu_service"
                android:resource="@xml/apduservice" />
        </service>
    </application>

</manifest>

apduservice.xml

<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/servicedesc"
    android:requireDeviceUnlock="false">
    <aid-group
        android:description="@string/aiddescription"
        android:category="other">
        <aid-filter android:name="F1234567890ABC" />
    </aid-group>
</host-apdu-service>

NdefHostApduService.java

package com.example.ndefemu002;

import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.util.Log;

import java.util.Arrays;

public class NdefHostApduService extends HostApduService {
    // アプリケーション選択のC-APDU
    private final static byte[] SELECT_APP = new byte[] {
            (byte) 0x00, // CLA
            (byte) 0xA4, // INS
            (byte) 0x04, // P1
            (byte) 0x00, // P2
            (byte) 0x07,

            // AID
            (byte) 0xF1,
            (byte) 0x23,
            (byte) 0x45,
            (byte) 0x67,
            (byte) 0x89,
            (byte) 0x0A,
            (byte) 0xBC
    };

    // CCファイル選択のC-APDU)
    private final static byte[] SELECT_CC_FILE = new byte[] {
            (byte)0x00,
            (byte)0xa4,
            (byte)0x00,
            (byte)0x0c,
            (byte)0x02,
            (byte)0xe1,
            (byte)0x03,
    };
    // NDEFレコードファイル選択のC-APDU
    private final static byte[] SELECT_NDEF_FILE = new byte[] {
            (byte)0x00,
            (byte)0xa4,
            (byte)0x00,
            (byte)0x0c,
            (byte)0x02,
            (byte)0xe1,
            (byte)0x04,
    };

    // 成功時の Status Word (レスポンスで使用する)
    private final static byte[] SUCCESS_SW = new byte[] {
            (byte)0x90,
            (byte)0x00,
    };
    // 失敗時の Status Word (レスポンスで使用する)
    private final static byte[] FAILURE_SW = new byte[] {
            (byte)0x6a,
            (byte)0x82,
    };

    // CCファイルのデータ
    private final static byte[] CC_FILE = new byte[] {
            0x00, 0x0f, // CCLEN
            0x20, // Mapping Version
            0x00, 0x3b, // Maximum R-APDU data size
            0x00, 0x34, // Maximum C-APDU data size
            0x04, 0x06, // Tag & Length
            (byte)0xe1, 0x04, // NDEF File Identifier
            0x00, 0x32, // Maximum NDEF size
            0x00, // NDEF file read access granted
            (byte)0xff, // NDEF File write access denied
    };

    // NDEFレコードファイル用変数
    private byte[] ndefRecordFile;

    // アプリが選択されているかどうかのフラグ
    private boolean appSelected;

    // CCファイルが選択されているかどうかのフラグ
    private boolean ccSelected;

    // NDEFレコードファイルが選択されているかどうかのフラグ
    private boolean ndefSelected;

    @Override
    public void onCreate() {
        Log.i("onCreate", "onCreate");
        super.onCreate();

        // 状態のクリア
        appSelected = false;
        ccSelected = false;
        ndefSelected = false;

        // NDEFレコードファイルの生成
        NdefRecord record = NdefRecord.createTextRecord("en", "Hello NFC.");
        NdefMessage ndefMessage = new NdefMessage(record);

        int nlen = ndefMessage.getByteArrayLength();

        ndefRecordFile = new byte[nlen + 2];
        ndefRecordFile[0] = (byte)(0xff & (nlen >> (8*(1-0))));
        ndefRecordFile[1] = (byte)(0xff & (nlen >> (8*(1-1))));
        System.arraycopy(ndefMessage.toByteArray(), 0, ndefRecordFile, 2, ndefMessage.getByteArrayLength());
    }


    //@Override
    public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
        if (Arrays.equals(SELECT_APP, commandApdu)) {
            // アプリ選択
            Log.i("processCommandApdu", "SELECT_APP");
            appSelected = true;
            ccSelected = false;
            ndefSelected = false;
            return SUCCESS_SW;
        } else if (appSelected && Arrays.equals(SELECT_CC_FILE, commandApdu)) {
            // CCファイル選択
            Log.i("processCommandApdu", "SELECT_CC_FILE");
            ccSelected = true;
            ndefSelected = false;
            return SUCCESS_SW;
        } else if (appSelected && Arrays.equals(SELECT_NDEF_FILE, commandApdu)) {
            // NDEFファイル選択
            Log.i("processCommandApdu", "SELECT_NDEF_FILE");
            ccSelected = false;
            ndefSelected = true;
            return SUCCESS_SW;
        } else if (commandApdu[0] == (byte)0x00 && commandApdu[1] == (byte)0xb0) {
            // READ_BINARY
            Log.i("processCommandApdu", "READ_BINARY");
            // オフセットと長さ
            int ofs =
                    ((0xff & commandApdu[2]) << 8) |
                    ((0xff & commandApdu[3]) << 0);
            int len = 0xff & commandApdu[4];

            // レスポンス用バッファ
            byte[] responseApdu = new byte[len + SUCCESS_SW.length];

            if (ccSelected || ndefSelected) {
                byte[] src = CC_FILE;
                if (ndefSelected) {
                    src = ndefRecordFile;
                }
                if ((ofs + len) <= src.length) {
                    System.arraycopy(src, ofs, responseApdu, 0, len);
                    System.arraycopy(SUCCESS_SW, 0, responseApdu, len, SUCCESS_SW.length);
                    return responseApdu;
                }
            }
        }

        return FAILURE_SW;
    }

    @Override
    public void onDeactivated(int reason) {
        Log.i("onDeactivated", "onDeactivated");
        appSelected = false;
        ccSelected = false;
        ndefSelected = false;
    }
}

※Nexus7 (2013)(Android6.0.1)で動作確認を行いました。