はじめに
去年のアドベントカレンダーでAndroidのNFCでMifare Ultralightのタグを読み取る方法について以下の記事を書きました。
「ICカードだけじゃない!? AndroidのNFCで読み書きしてみた」
https://qiita.com/kurotsu/items/0748961a59a33a045d33
1年ぶりですが、続編ということで今度は皆さんにもおなじみのSuicaを使ってみたいと思います。
NFCやAndroidのAPIに関して基本的な事柄を去年の記事で解説しています。
また、画面遷移について同じ実装でSuicaの情報を読み取るところだけ追加していきますので、
一度、去年の記事を参照してみてください。
Suicaを読み取る実装の追加
タグを検出して、画面のタブを切替えてからFragment①に処理が切り替わるまでは同じです。
検出したタグによって処理を分ける部分を再度、掲載しておきます。
// NDEF information
if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
String[] techList = tag.getTechList();
for (String tech : techList) {
if(tech.equals("android.nfc.tech.NfcA")){
techFragment.setTechAFragment(intent);
}
if(tech.equals("android.nfc.tech.NfcB")){
techFragment.setTechBFragment(intent);
}
if(tech.equals("android.nfc.tech.NfcF")){
techFragment.setTechFFragment(intent);
}
}
procNdef(intent);
// Felica information
} else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {
Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
String[] techList = tag.getTechList();
String techSelect = "";
for (String tech : techList) {
if(tech.equals("android.nfc.tech.NfcA")){
techFragment.setTechAFragment(intent);
techSelect = tech;
}
if(tech.equals("android.nfc.tech.NfcB")){
techFragment.setTechBFragment(intent);
techSelect = tech;
}
if(tech.equals("android.nfc.tech.NfcF")){
techFragment.setTechFFragment(intent);
techSelect = tech;
}
}
// Felica Card
if(techSelect.equals("android.nfc.tech.NfcF")){
procFelica(intent);
}
if(techSelect.equals("android.nfc.tech.NfcB")){
IsoDep isoDep = IsoDep.get(tag);
if(isoDep != null){
procDriversCard(intent);
}
}
// Mifare Classic Card
if(techSelect.equals("android.nfc.tech.NfcA")){
MifareClassic mfClassic = MifareClassic.get(tag);
if(mfClassic != null){
procMfClassic(intent);
}
}
Suicaのカードをかざした場合はタグのtechListに"android.nfc.tech.NfcF"が設定されてきます。
procFelica(intent)の実装をして読み取っていきます。
/**
* Felica Cardの処理
*
* @param intent
*/
private void procFelica(final Intent intent) {
byte[] idm = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
NfcF nfcF = NfcF.get(tag);
byte[] systemCode = nfcF.getSystemCode();
StringBuilder sb = new StringBuilder();
StringBuilder sb2 = new StringBuilder();
StringBuilder sb3 = new StringBuilder();
// Suica Transaction
if(byteToInt(systemCode) == 0x0003){
byte addr = 0;
int serviceCode = 0x090f; // サービスコマンド(Suica履歴情報)
byte[] serviceCmd = new byte[]{(byte) (serviceCode & 0xff), (byte) (serviceCode >> 8)};
byte[] read_wo_encryption_command = new byte[]{(byte)0x06}; // 非暗号化領域読込コマンド read_wo_encryption_command:0x06
byte[] data = new byte[]{
(byte) 0x01, // サービス数
(byte) serviceCmd[0], // サービスコード (little endian)
(byte) serviceCmd[1],
(byte) 0x01, // 同時読み込みブロック数
(byte) 0x80, addr // ブロックリスト
};
int length = 16; // コマンド長(length(1)+ idm(8)+ cmd(1)+ data(6))
ByteBuffer buff = ByteBuffer.allocate(length);
byte byteLength = (byte)length;
buff.put(byteLength).put(read_wo_encryption_command).put(idm).put(data);
byte[] tranCmd = buff.array();
byte[] response;
Long remain;
SimpleDateFormat sdf;
String strDate;
byte[] contents;
NumberFormat nf = NumberFormat.getCurrencyInstance();
nf.setMaximumFractionDigits(0);
try {
nfcF.connect();
response = nfcF.transceive(tranCmd);
while(response != null){
if(response.length < 13){
break;
}
if((response[13] & 0xff) == 0xc7 || (response[13] & 0xff) == 0xc8){
sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss",Locale.JAPANESE);
strDate = sdf.format(getProccessDate(byteToInt(new byte[]{response[17], response[18]}),byteToInt(new byte[]{response[19], response[20]})));
} else {
sdf = new SimpleDateFormat("yyyy/MM/dd",Locale.JAPANESE);
strDate = sdf.format(getProccessDate(byteToInt(new byte[]{response[17], response[18]}),0));
}
sb.append("日時:" + strDate + "\n");
int cType = byteToInt(new byte[]{response[13]});
sb.append("機器:" + getConsoleType(cType) + "\n");
int proc = byteToInt(new byte[]{response[14]});
sb.append("処理:" + getProcessType(proc) + "\n");
remain = Long.valueOf((byteToInt(new byte[]{response[24], response[23]})));
sb.append("残高:" + nf.format(remain) + "\n");
sb.append("残高(b):");
sb.append(String.format("%02X", response[24]));
sb.append(String.format("%02X", response[23]));
sb.append("\n");
contents = Arrays.copyOfRange(response, 10, response.length);
for (byte b : contents) {
sb.append(String.format("%02X", b));
}
sb.append("\n");
sb.append("---------------------------------------------------------\n");
addr++;
tranCmd[tranCmd.length-1] = addr;
response = nfcF.transceive(tranCmd);
}
} catch(Exception e){
e.printStackTrace();
} finally {
try {
nfcF.close();
} catch(Exception e){
e.printStackTrace();
}
}
// nanaco Transaction
} else if(byteToInt(systemCode) == 0x04c7){
// TODO: nanaco implementation
} else {
sb.append("none\n");
}
setFelicaTransactionBody(sb.toString());
// System Code List
byte[] command_request_systemcode = new byte[]{(byte)0x0c}; // SystemCode検索コマンド command_request_systemcode:0x0c
int length = 10; // コマンド長(length(1)+ idm(8)+ cmd(1))
ByteBuffer buff = ByteBuffer.allocate(length);
byte byteLength = (byte)length;
buff.put(byteLength).put(command_request_systemcode).put(idm);
byte[] systemCodeCmd = buff.array();
byte[] response;
try {
nfcF.connect();
response = nfcF.transceive(systemCodeCmd);
int array_length = (int)response[10];
byte[] serviceCodes;
for (int i=0; i < array_length; i++) {
serviceCodes = Arrays.copyOfRange(response, 11+i*2, 13+i*2);
for (byte b : serviceCodes) {
sb2.append(String.format("%02X", b));
}
sb2.append("\n");
}
} catch(Exception e){
e.printStackTrace();
} finally {
try {
nfcF.close();
} catch(Exception e){
e.printStackTrace();
}
}
setFelicaSystemCodeBody(sb2.toString());
// Service Command List
byte[] command_search_servicecode = new byte[]{(byte)0x0a}; // SystemCode検索コマンド command_search_servicecode:0x0a
length = 12; // コマンド長(length(1)+ idm(8)+ cmd(1) + index(2))
int addr = 0;
byte[] indexByte = new byte[]{(byte)(addr & 0xff), (byte)(addr >> 8)};
buff = ByteBuffer.allocate(length);
byteLength = (byte)length;
buff.put(byteLength).put(command_search_servicecode).put(idm).put(indexByte);
byte[] serviceCodeCmd = buff.array();
byte[] contents;
byte[] serviceCode = new byte[2];
try {
nfcF.connect();
response = nfcF.transceive(serviceCodeCmd);
while(response != null){
contents = Arrays.copyOfRange(response, 10, response.length);
if (contents.length != 2 && contents.length != 4) break; // 2 or 4 バイトじゃない場合終了
if (contents.length == 2) {
if (contents[0] == (byte)0xff && contents[1] == (byte)0xff) break; // FFFF が終了コード
// little endian
serviceCode[0] = contents[1];
serviceCode[1] = contents[0];
for (byte b : serviceCode) {
sb3.append(String.format("%02X", b));
}
int accessInfo = serviceCode[1] & 0x3F; // 下位6bitがアクセス情報
switch (accessInfo) {
case 0x09: sb3.append(" 固定長RW"); break; // RW: ReadWrite
case 0x0b: sb3.append(" 固定長RO"); break; // RO: ReadOnly
case 0x0d: sb3.append(" 循環RW"); break;
case 0x0f: sb3.append(" 循環RO"); break;
case 0x11: sb3.append(" 加減算直接"); break;
case 0x13: sb3.append(" 加減算戻入"); break;
case 0x15: sb3.append(" 加減算減算"); break;
case 0x17: sb3.append(" 加減算RO"); break;
case 0x08: sb3.append(" 固定長RW(Locked)"); break; // RW: ReadWrite
case 0x0a: sb3.append(" 固定長RO(Locked)"); break; // RO: ReadOnly
case 0x0c: sb3.append(" 循環RW(Locked)"); break;
case 0x0e: sb3.append(" 循環RO(Locked)"); break;
case 0x10: sb3.append(" 加減算直接(Locked)"); break;
case 0x12: sb3.append(" 加減算戻入(Locked)"); break;
case 0x14: sb3.append(" 加減算減算(Locked)"); break;
case 0x16: sb3.append(" 加減算RO(Locked)"); break;
}
sb3.append("\n");
}
addr++;
serviceCodeCmd[serviceCodeCmd.length-2] = (byte)(addr & 0xff);
serviceCodeCmd[serviceCodeCmd.length-1] = (byte)(addr >> 8);
response = nfcF.transceive(serviceCodeCmd);
}
} catch(Exception e){
e.printStackTrace();
} finally {
try {
nfcF.close();
} catch(Exception e){
e.printStackTrace();
}
}
setFelicaServiceCodeBody(sb3.toString());
}
Felicaのカードにはシステムコードと呼ばれるカードの種類によって決められたコードが規定されています。
Suicaは”0003”です。NanacoやWAONなどFelicaを採用しているカードにもそれぞれシステムコードが割り振られています。
それから、カードの中の情報についてはアクセス先を表すサービスコードが設定されています。
Suicaのカードでチャージをしたり、タッチして品物を購入したときの履歴情報は”090f”です。
サービスコードやidmを利用して、AndroidのnfcFのAPIを使ってコマンドを送信してレスポンスを受け取ります。
ブロックアドレスを変更しながら、最大20件の履歴情報を読み取ることが出来ます。
「// System Code List」のコメントから先はカードに設定されているシステムコードの一覧を表示しています。
Suicaでは”0003”の他にもコードが設定されているようです。
「// Service Command List」のコメントから先はカードに設定されているサービスコードの一覧を表示しています。
この情報からアクセスできる情報のサービスコードを確認することができます。
履歴情報は”090f”は循環型のデータ構造でReadOnlyで非暗号化の領域ということがわかります。
取得した情報を設定している処理が下記になります。
/**
* Felica履歴 の表示 - ページ
*
* @param page
*/
private void setFelicaTransactionBody(String page) {
map = new HashMap<String, String>();
map.put("Title", "Transaction");
groupData.add(map);
map = new HashMap<String, String>();
map.put("Text", page);
ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
list.add(map);
childData.add(list);
lv.expandGroup(0);
}
/**
* Felica System Code の一覧 - ページ
*
* @param page
*/
private void setFelicaSystemCodeBody(String page) {
map = new HashMap<String, String>();
map.put("Title", "System Code List");
groupData.add(map);
map = new HashMap<String, String>();
map.put("Text", page);
ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
list.add(map);
childData.add(list);
lv.expandGroup(1);
}
/**
* Felica Service Code の一覧 - ページ
*
* @param page
*/
private void setFelicaServiceCodeBody(String page) {
map = new HashMap<String, String>();
map.put("Title", "Service Code List");
groupData.add(map);
map = new HashMap<String, String>();
map.put("Text", page);
ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
list.add(map);
childData.add(list);
lv.expandGroup(2);
}
履歴情報のタイプを変換するメソッドが下記となります。
コメントがあるとおり、有志の方が調べてくれた情報の下記サイトを参考にしています。
http://sourceforge.jp/projects/felicalib/wiki/suica
だいぶ前に実装したものなので、適宜情報を最新にしてみてください。
/**
* 機器種別を取得します
*
* <pre>
* http://sourceforge.jp/projects/felicalib/wiki/suicaを参考にしています
* </pre>
*
* @param cType
* コンソールタイプをセット
* @return String 機器タイプが文字列で戻ります
*/
public static final String getConsoleType(int cType) {
switch (cType & 0xff) {
case 0x03:
return "精算機";
case 0x04:
return "携帯型端末";
case 0x05:
return "等車載端末"; // bus
case 0x07:
return "券売機";
case 0x08:
return "券売機";
case 0x09:
return "入金機(クイックチャージ機)";
case 0x12:
return "券売機(東京モノレール)";
case 0x13:
return "券売機等";
case 0x14:
return "券売機等";
case 0x15:
return "券売機等";
case 0x16:
return "改札機";
case 0x17:
return "簡易改札機";
case 0x18:
return "窓口端末";
case 0x19:
return "窓口端末(みどりの窓口)";
case 0x1a:
return "改札端末";
case 0x1b:
return "携帯電話";
case 0x1c:
return "乗継清算機";
case 0x1d:
return "連絡改札機";
case 0x1f:
return "簡易入金機";
case 0x46:
return "VIEW ALTTE";
case 0x48:
return "VIEW ALTTE";
case 0xc7:
return "物販端末"; // sales
case 0xc8:
return "自販機"; // sales
default:
return "???";
}
}
/**
* 処理種別を取得します
*
* <pre>
* http://sourceforge.jp/projects/felicalib/wiki/suicaを参考にしています
* </pre>
*
* @param proc
* 処理タイプをセット
* @return String 処理タイプが文字列で戻ります
*/
public static final String getProcessType(int proc) {
switch (proc & 0xff) {
case 0x01:
return "運賃支払(改札出場)";
case 0x02:
return "チャージ";
case 0x03:
return "券購(磁気券購入";
case 0x04:
return "精算";
case 0x05:
return "精算(入場精算)";
case 0x06:
return "窓出(改札窓口処理)";
case 0x07:
return "新規(新規発行)";
case 0x08:
return "控除(窓口控除)";
case 0x0d:
return "バス(PiTaPa系)"; // byBus
case 0x0f:
return "バス(IruCa系)"; // byBus
case 0x11:
return "再発(再発行処理)";
case 0x13:
return "支払(新幹線利用)";
case 0x14:
return "入A(入場時オートチャージ)";
case 0x15:
return "出A(出場時オートチャージ)";
case 0x1f:
return "入金(バスチャージ)"; // byBus
case 0x23:
return "券購 (バス路面電車企画券購入)"; // byBus
case 0x46:
return "物販"; // sales
case 0x48:
return "特典(特典チャージ)";
case 0x49:
return "入金(レジ入金)"; // sales
case 0x4a:
return "物販取消"; // sales
case 0x4b:
return "入物 (入場物販)"; // sales
case 0xc6:
return "物現 (現金併用物販)"; // sales
case 0xcb:
return "入物 (入場現金併用物販)"; // sales
case 0x84:
return "精算 (他社精算)";
case 0x85:
return "精算 (他社入場精算)";
default:
return "???";
}
}
Fragment②ではFelicaの技術情報を追加します。
byte[] idm = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
sb1.append("ID:");
if (idm != null){
for (byte b : idm) {
sb1.append(String.format("%02X", b));
}
}
sb1.append("\n");
Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
NfcF nfcF = NfcF.get(tag);
byte[] manufacturer = nfcF.getManufacturer();
if(manufacturer != null){
sb1.append("PMm : ");
for (byte b : manufacturer) {
sb1.append(String.format("%02X", b));
}
sb1.append("\n");
}
sb2.append("JIS X 6319-4 compatible\n");
byte[] data = new byte[1000];
try {
nfcF.connect();
nfcF.transceive(data);
} catch(Exception e){
e.printStackTrace();
} finally {
try {
nfcF.close();
} catch(Exception e){
e.printStackTrace();
}
}
int tran_length = nfcF.getMaxTransceiveLength();
if(tran_length < 0){
sb2.append("▶Maximum transceive length: unknown\n");
} else {
sb2.append("▶Maximum transceive length: " + Integer.valueOf(tran_length) + "bytes\n");
}
int timeout = nfcF.getTimeout();
sb2.append("▶Maximum transceive time-out: " + Integer.valueOf(timeout) + "ms\n");
byte[] systemCode = nfcF.getSystemCode();
if(systemCode != null){
sb3.append("System Code : ");
for (byte b : systemCode) {
sb3.append(String.format("%02X", b));
}
sb3.append("\n");
}
if(NdefReadFragment.byteToInt(systemCode) == 0x0003){
sb3.append("Card Type : Suica\n");
}
String[] techList = tag.getTechList();
for(int i = 0; i <= techList.length - 1; i++){
sb4.append("▶" + techList[i]);
if(i < techList.length - 1) sb4.append("\n");
}
setTechFPage(sb1.toString(),sb2.toString(),sb3.toString(),sb4.toString());
これで実装の説明は終わりです。
Suicaの履歴情報を読み取ってみる
いよいよ実際にSuicaを読み取りたいと思います。
Suicaをアプリで読み取って際の分割写真を張り付けています。
SCANタブの情報です。
TECHタブの情報が下記になります。
おわりに
実装の細部などは端折ってしまっていますが、皆さんで考えながら実装してみてください。
皆さんの手元にあるカードでFelicaを採用しているものもあるかと思います。
システムコードやサービスコードの情報がこの実装で読み取ることが出来ると思いますので、
実際にかざしてみて確認してみてください。