3
4

More than 5 years have passed since last update.

FeliCa(WAON)カードの情報を読んで見る

Posted at

やりたいこと

FeliCaカードの固有IDであるIDmを読むサンプルはよくあるが、各種電子マネーの情報を読むサンプルはあまりないので、手元にあったWAONカードの情報(とりあえずWAON番号)を読んでみます。

WAONカードはミニストップなどで300円で売っている。

前提

  • 言語はJavaを利用
  • 開発はAndroid Studio 3.4.x on Mac
  • 検証実機はXperia XZ Premium(SO-04j)
  • FeliCa(NFC)の操作法に重点を置きたいのでエラー処理とかは最低限
  • IDmの取得もtag.getId()とかではなくFeliCaコマンドで行う

仕様

以下のような感じとする。

  • idm, pmm, WAON番号表示用のTextViewを配置
  • polling start, stopボタンを配置
  • polling startボタンを押したらpollingを開始し、カードがかざされたら各種IDを取得・表示
  • polling stopボタンでpollingを停止

見た目は以下のような感じ。

スクリーンショット 2019-05-31 9.07.07.png

処理の流れ

カードを認識して読み取るまでの大まかな流れは以下の通り。

  • NfcAdapterを生成し、ReaderModeで読み取る(読み取りを待ち受ける)
  • 認識されたカードはTagとして取得される
  • Tagを指定してNfcFを生成し、通信を行う(各種コマンドを実行する)
  • Pollingコマンドでカードの認識ID(IDm)を取得
  • 対象特定のためIDmを添付して、読み取りコマンド(readWithoutEncryption)を実行
  • 取得した内容を表示するなどする

準備

NFC機能を利用するにはAndroidManifext.xmlに以下の追記。

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

そもそも、端末自体のReader/Writer機能をOnにする必要もあります。

実装

雑に実装してみます。

MainActivity.java

今回はボタンで読み取り機能のOn/OffをしたいのでReaderModeを利用しています。
そのenable/disableをstartボタン、stopボタンでそれぞれ制御しています。

カード(Tag)が認識されたらCallbackクラスのonTagDiscovered()が呼び出されるので、その中で必要な処理をします。
FeliCaコマンドを実行するにはtranscive()を利用します。今回はpollingとreadWithoutEncryptionの2つを実行しています。

pollingは通信先のカードID(IDm)を特定するために必須となります。
readWithoutEncryptionでは読み取る「場所」をサービスコードで指定する必要があります。今回はWAON番号(カード番号)を読み取りたいので0x68, 0x4fを指定しています。

サービスコードは公開されていませんが、有志によって調べられています

WAONの他のサービス(残高とか)、他のカードの情報を利用したい場合、サービスコードやフォーマットを入手しましょう。

なお、FeliCaには鍵あり領域と鍵なし領域があり、チャージ等の領域は「鍵あり領域」に保持されているため第三者が操作することはできません。念の為。

MainActivity.java
package jp.bluecode.waon_java;

import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.NfcF;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Formatter;

public class MainActivity extends AppCompatActivity {

    //Widgetの宣言
    TextView txt_idm;
    TextView txt_pmm;
    TextView txt_waonno;
    Button btn_start;
    Button btn_stop;

    //NfcAdapterの宣言
    NfcAdapter nfcAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Widgetの初期化
        txt_idm = findViewById(R.id.txt_idm);
        txt_pmm = findViewById(R.id.txt_pmm);
        txt_waonno = findViewById(R.id.txt_waonno);
        btn_start = findViewById(R.id.btn_start);
        btn_stop = findViewById(R.id.btn_stop);

        //初期設定(トグル)
        btn_stop.setEnabled(false);

        //NfcAdapterの初期化
        nfcAdapter = NfcAdapter.getDefaultAdapter(this);

        //onClickイベントの設定
        btn_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                //トグル設定
                btn_start.setEnabled(false);
                btn_stop.setEnabled(true);

                //TextView初期化
                txt_idm.setText("");
                txt_pmm.setText("");
                txt_waonno.setText("");

                //ReaderMode On
                nfcAdapter.enableReaderMode(MainActivity.this,new MyReaderCallback(),NfcAdapter.FLAG_READER_NFC_F,null);

            }
        });

        btn_stop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                //トグル設定
                btn_start.setEnabled(true);
                btn_stop.setEnabled(false);

                //ReaderMode Off
                nfcAdapter.disableReaderMode(MainActivity.this);

            }
        });

    }

    //Callback用 inner class記述
    private class MyReaderCallback implements NfcAdapter.ReaderCallback{
        @Override
        public void onTagDiscovered(Tag tag){

            //タグが見つかったらとりあえずログ出力
            Log.d("Hoge","Tag Discovered!");

            //FeliCaと通信するためのNfcFを初期化
            NfcF nfc = NfcF.get(tag);

            try{

                nfc.connect();

                //とりあえずいろいろFeliCaの生コマンドで制御。idmとかならtag.getId()とかで取れる。

                //pollingコマンド(FeliCa生コマンド) 共通領域のシステムコード0xFE 0x00を指定。
                byte[] polling_request = {(byte)0x06,(byte)0x00,(byte)0xFE,(byte)0x00,(byte)0x00,(byte)0x00};
                //response受け取り用byte配列

                //コマンド送信・受信
                byte[] polling_response = nfc.transceive(polling_request);

                //idmの取り出し
                byte[] idm = Arrays.copyOfRange(polling_response,2,10);
                //pmmの取り出し(ついで)
                byte[] pmm = Arrays.copyOfRange(polling_response,11,19);

                //byte列を文字列に変換
                final String idmString = bytesToHexString(idm);
                final String pmmString = bytesToHexString(pmm);

                //waonno処理

                //waonnno request
                //カスタム関数をつかってrequestコマンドを組み立て
                byte[] waonno_request = readWithoutEncryption(idm,2);
                //コマンド送信・受信
                byte[] wannno_response = nfc.transceive(waonno_request);

                //WAON番号部分を切り取り
                byte[] waonno = Arrays.copyOfRange(wannno_response,13,21);

                //文字列変換
                final String waonnoString = bytesToHexString(waonno);

                //親スレッドのUI更新
                Handler mainHandler = new Handler(Looper.getMainLooper());
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        txt_idm.setText(idmString);
                        txt_pmm.setText(pmmString);
                        txt_waonno.setText(waonnoString);
                    }
                });

                nfc.close();

            }catch(Exception e){
                Log.e("Hoge",e.getMessage());
            }
        }
    }

    //非暗号領域読み取りコマンド(WAON番号領域特化)
    private byte[] readWithoutEncryption(byte[] idm, int blocksize) throws IOException {

        ByteArrayOutputStream bout = new ByteArrayOutputStream(100); //とりあえず

        //readWithoutEncryptionコマンド組み立て
        bout.write(0); //コマンド長(後で入れる)
        bout.write(0x06); //0x06はRead Without Encryptionを表す
        bout.write(idm); //8byte:idm
        bout.write(1); //サービス数
        bout.write(0x4f); //サービスコードリスト WAONカード番号は684F
        bout.write(0x68); //サービスコードリスト
        bout.write(blocksize); //ブロック数

        for(int i=0; i<blocksize; i++){
            bout.write(0x80); //ブロックリスト
            bout.write(i);
        }

        byte[] msg = bout.toByteArray();
        msg[0] = (byte)msg.length;

        return msg;
    }

    //bytesを16進数型文字列に変換用関数
    private String bytesToHexString(byte[] bytes){
        StringBuilder sb = new StringBuilder();
        Formatter formatter = new Formatter(sb);
        for(byte b: bytes){
            formatter.format("%02x",b);
        }
        //大文字にして戻す(見た目の調整だけ)
        return sb.toString().toUpperCase();
    }
}

以上です。

activity_main.xml

参考程度にactivity_main.xmlも記載しておきますが、お好みでレイアウトしたほうがいいでしょう。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/lbl_top"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="WAON番号 Viewer"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/lbl_idm"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="68dp"
        android:layout_marginTop="32dp"
        android:text="IDm:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/lbl_top" />

    <TextView
        android:id="@+id/txt_idm"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="32dp"
        android:background="#E8EAF6"
        app:layout_constraintStart_toEndOf="@+id/lbl_idm"
        app:layout_constraintTop_toBottomOf="@+id/lbl_top" />

    <TextView
        android:id="@+id/lbl_pmm"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="60dp"
        android:layout_marginTop="24dp"
        android:text="PMm:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/lbl_idm" />

    <TextView
        android:id="@+id/txt_pmm"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="24dp"
        android:background="#E8EAF6"
        app:layout_constraintStart_toEndOf="@+id/lbl_pmm"
        app:layout_constraintTop_toBottomOf="@+id/txt_idm" />

    <TextView
        android:id="@+id/lbl_wanno"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="24dp"
        android:text="WAON番号:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/lbl_pmm" />

    <TextView
        android:id="@+id/txt_waonno"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="24dp"
        android:background="#F3E5F5"
        app:layout_constraintStart_toEndOf="@+id/lbl_wanno"
        app:layout_constraintTop_toBottomOf="@+id/txt_pmm" />

    <Button
        android:id="@+id/btn_start"
        android:layout_width="160dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="Start Polling"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txt_waonno" />

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="160dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="Stop Polling"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_start" />
</android.support.constraint.ConstraintLayout>

AndroidManifest.xml

こちらも参考程度に。編集しているのは、冒頭に触れたNFCのパーミッションだけです。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.bluecode.waon_java">

    <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>
    </application>

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

</manifest>
3
4
0

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
3
4