LoginSignup
29

More than 3 years have passed since last update.

Raspberry Pi + PaSoRi と Android HCE で NFC 通信

Last updated at Posted at 2019-02-16

はじめに

タイトルのような環境で,APDUを用いたHCEとの通信を行いたかったのでメモ.

Raspberry Piとは

美味しいお菓子.
https://www.raspberrypi.org/

PaSoRiとは

PCでNFCが扱えるやべーやつ.
https://www.sony.co.jp/Products/felica/consumer/products/RC-S380.html

Androidとは

iOSじゃないやつ.
https://www.android.com/

HCEとは

Host Card Emulationの略で,Android端末をNFCタグとして扱う方法.
APDUという規格を使って,自由なデータ通信もできる.
今回はHCE-F(FeliCa準拠)じゃないほうを使う.
https://developer.android.com/guide/topics/connectivity/nfc/hce

環境

  • Raspberry Pi 3 Model B+
    • Arch Linux ARMv7 (Kernel 4.14.90-1-ARCH)
  • SONY PaSoRi RC-S380
  • ASUS Zenfone 4 Z01KDA/ZE554KL
    • Android 8.0.0

準備

Pythonのインストール

nfcpyはPython 3系に対応していないので,2系を使う.
v1.0.0 で Python 3 系に対応しました.

# pacman -Syu python python-pip

nfcpyのインストール

# pip install nfcpy

テスト

nfcpyのGitリポジトリからサンプルを入手して実行する.

# pacman -Syu git
$ git clone https://github.com/nfcpy/nfcpy
$ cd nfcpy/examples
$ python tagtool.py
Output
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb
[nfc.clf] no reader available on path usb
[main] no contactless reader found on usb
[main] no contactless reader available

リーダが見つからない,と言われるので,lsusbで確認してみる.

$ lsusb
Output
Bus 001 Device 004: ID 054c:06c3 Sony Corp. RC-S380
Bus 001 Device 003: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 002: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

しっかり認識されている.
では,デバイスIDを指定して実行してみる.

$ python tagtool.py --device usb:054c:06c3
Output
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb:054c:06c3
[main] access denied for device with path usb:054c:06c3
[main] no contactless reader available

今度はアクセスが拒否された.
デバイスIDを指定しないと,アクセスが拒否されたリーダは無視するようだ.
udevのルールを用いて,plugdevグループのユーザにリーダの使用を許可する.

# echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules

リーダを使うユーザがplugdevグループに参加していなければならないので,確認する.

$ groups
Output
wheel siketyan

グループがない,もしくは参加していなければ,作成および参加を行う.

# groupadd plugdev
# usermod -aG plugdev siketyan

接続しなおすか,再起動して再確認.

$ python tagtool.py --device usb:054c:06c3
Output
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb:054c:06c3
[main] the reader on usb:054c:06c3 is busy
[main] no contactless reader available

今度は,リーダがビジーだと言われた.
カーネルモジュールのport100と競合しているようなので,無効化して再確認.

# rmmod port100
$ python tagtool.py
Output
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb
[nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:001:005
** waiting for a tag **

タグ待ちに入った!
Android端末をタッチしてみる.

Output
Type4ATag MIU=255 FWT=0.038664

ちゃんと認識したみたい!

攻めと受け

NFCにおいてどちらが攻めでどちらが受けかという問題は,けっこう難しい.
今回の場合,先に電波を出してタグを探すのはPaSoRi側なので,こちらが攻めかもしれない.
ただ,物理的にはタグを近づけていくのでこちらが攻めかもしれない.
ここでは,PaSoRi側を攻め,タグ側を受けと仮定して話を進める.

Android HCEで受け側をつくる

Google信者の方はAndroid Studio,JetBrains信者の方はIntelliJ IDEAで,新規Androidプロジェクトを作成する.
Android SDKのセットアップは情報が多いと思うので適当に.

まずはManifestを書く.
今回はNFCによってバイブレータ動作させたいので,VIBRATE権限を要求.

res/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.sikeserver.nfc.test">
    <uses-feature
            android:name="android.hardware.nfc.hce"
            android:required="true"
            tools:targetApi="eclair" />

    <uses-permission android:name="android.permission.NFC" />
    <uses-permission android:name="android.permission.VIBRATE"/>
    <application android:allowBackup="true"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:targetApi="donut">
        <service
            android:name=".service.ApduService"
            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>
res/xml/apduservice.xml
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_name"
    android:requireDeviceUnlock="false">
    <aid-group
        android:description="@string/app_name"
        android:category="other">
        <aid-filter android:name="F1145141919810" />
    </aid-group>
</host-apdu-service>

Manifestで指定したサービスをJavaで書く.

main/java/service/com/sikeserver/nfc/test/service/ApduService.java
package com.sikeserver.nfc.test.service

import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;

import java.util.Arrays;

public class ApduService extends HostApduService {
    private static final byte[] COMMAND_SELECT = new byte[] {
        (byte) 0x00, // CLA
        (byte) 0xA4, // INS
        (byte) 0x04, // P1
        (byte) 0x00, // P2
        (byte) 0x07,

        // AID
        (byte) 0xF1,
        (byte) 0x14,
        (byte) 0x51,
        (byte) 0x41,
        (byte) 0x91,
        (byte) 0x98,
        (byte) 0x10
    };

    private static final byte[] COMMAND_VIBRATE = new byte[] {
        (byte) 0x00, // CLA
        (byte) 0x11, // INS
        (byte) 0x45, // P1
        (byte) 0x14  // P2
    };

    private static final byte[] RESPONSE_OK = new byte[] {
        (byte) 0x90, // SW1
        (byte) 0x00  // SW2
    };

    @Override
    public byte[] processCommandApdu(byte[] command, Bundle extras) {
        if (Arrays.equals(command, COMMAND_SELECT)) {
            Log.d("NFCTest", "AID Selected");
        } else if (Arrays.equals(command, COMMAND_VIBRATE)) {
            Log.d("NFCTest", "Vibrate");

            Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
            if (vibrator != null) {
                vibrator.vibrate(
                    VibrationEffect.createOneShot(
                        100,
                        VibrationEffect.DEFAULT_AMPLITUDE
                    )
                );
            }
        }

        return RESPONSE_OK;
    }
}

Pythonで攻め側をつくる

がーって書く.
詳しくはnfcpyのドキュメントを参照のこと.
AIDとバイブレートコマンドは適宜書き換えて.

import nfc


def select(tag):
    result = tag.send_apdu(
        0x00, # CLA
        0xA4, # INS
        0x04, # P1
        0x00, # P2
        bytearray.fromhex('F1145141919810'), # AID
        check_status=False
    )

    return True


def vibrate(tag):
    result = tag.send_apdu(
        0x00, # CLA
        0x11, # INS
        0x45, # P1
        0x14, # P2
        check_status=False
    )

    return True


def startup(targets):
    return targets


def connected(tag):
    print("Tag Connected")

    select(tag)
    vibrate(tag)

    return True


def released(tag):
    print('Tag Released')


def main():
    clf = nfc.ContactlessFrontend('usb')
    print(clf)
    if clf:
        while clf.connect(rdwr={
            'on-startup': startup,
            'on-connect': connected,
            'on-release': released,
        }):
            pass


if __name__ == '__main__':
    main()

まとめ

ピッってできるとたのしい!

参考

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
29