memorandums

日々の生活で問題解決したこと、知ってよかったことなどを自分が思い出すために記録しています。

ProcessingのAndroidモードでタグ読み込み

今春より学生証がFelicaに対応しました。いまのところの用途は出席管理です。リーダーはパナのAndroid端末なので、講義前に事務室に取りに行って講師用のカードをかざした後に学生さんらにタッチしてもらいます。いろいろと面倒ですが、個人的には悪くないなぁ。。。と思っています。

で、せっかくなので学生証を他の用途に利用できないかなぁ。。。と探索してきました。ここ1、2週間くらいの話です。

ゼミ室にNexus7があったので、NFC Taginfo by NXPをいれてカードのタグ情報を読み取ってみました。

以下のような感じです。

f:id:ke_takahashi:20150427232705p:plain

タグはNfcFでした。

最初はまったく知識がなかったのでSlideshareなど見ながら概要を勉強してあちこちのブログやSonyFelicaのページなどをあたりました。

とりあえずAndroidプログラミングが面倒だったので、ProcessingのAndroidモードを利用しようとしました(最初2.2.1ではなぜかAndroidモードがインストールできず3.0をいれて使っていましたが2.2.1に戻してみるとちゃんと表示されたり、よくわかりません)。

AndroidモードにはKetaiライブラリがあるのでNFCの読み込みも簡単じゃね?と思ったわけです。

NDEFというデータ構造があり、そこにIDが書かれているだろうと思ったんですね。Ketaiのソースをクローンしてあちこちにプリント文いれて解析してみましたが、結局はNDEFはなく、TECHというデータしかないことがわかりました。

こういうことがわかるのに結構時間がかかりました。。。ネットの記事やスライドは断片的でSonyの資料は詳しすぎる。ネットに以下の2つの本のPDFがあったのでこれが役立ちました。このPDFはドラフトなのかなぁ。。。?でも助かりました。英語ですが。。。

  • Beginning NFC(リンクが切れているような。。。)

で、話を戻して、カードのデータ構造はわかりませんが、僕のカードのIDはD0010なので。。。上記のTaginfoのスクショの緑枠の部分がそれらしいです。タグからこのデータを読み取りたい。。。これがなかなかできなかった。今日やっとできました。

Ketaiライブラリのソースを書き換えてjarを作成しなおして。。。とやっていましたが、最終的にそれは必要ないことがわかりました。僕がみる限り、KetaiのNFCクラスはTECHには対応していないようです。たぶん。

で、とりあえずうまくいったソースです。

前提としては、Processing2.2.1でAndriodモードに切り替えてKetaiライブラリをインストールします。そして、KetaiライブラリのNFCReadを読み込みます。そこに以下のソースを加えました。このプログラムを作るに際して以下のブログを参考にさせていただきました。まだわからないで書いているところもありますので。。。自信はありませんが。とりあえず動きましたということで。

Nexus7でPASMO(Suica)の履歴を読んでみた : 時々、失業SEの開発日誌

■ActivityCodeタブ

public void onCreate(Bundle savedInstanceState) { 
  super.onCreate(savedInstanceState);
  ketaiNFC = new KetaiNFC(this);
}

public void onNewIntent(Intent intent) { 
  if (ketaiNFC != null) {
    ketaiNFC.handleIntent(intent);
    textRead = processTECH(intent); //※ここを加えます。
  }
}

■TECH(新規タブ作成する)

import android.nfc.Tag;
import android.nfc.NfcAdapter;
import android.nfc.tech.NfcF;
import java.io.ByteArrayOutputStream;

String processTECH(Intent intent) 
{
  String ret = "";
  byte[] felicaIDm = new byte[] {0};
  Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
  if (tag != null) {
    felicaIDm = tag.getId();
  }
  NfcF nfc = NfcF.get(tag);
  try {
    nfc.connect();
    byte[] req = readWithoutEncryption(felicaIDm, 1);
    byte[] res = nfc.transceive(req);
    nfc.close();
    
    ret = parse(res);
    PApplet.println(ret);
  } 
  catch (Exception e) {
    println(e.toString());
  }
  
  return ret;
}

byte[] readWithoutEncryption(byte[] idm, int size) throws IOException 
{
  ByteArrayOutputStream bout = new ByteArrayOutputStream(100);
  bout.write(0); // the data length (dummy)
  bout.write(0x06); // "Read Without Encryption" (Felica Command)
  bout.write(idm); // IDm (8 bytes)
  bout.write(1); // Number of Service
  bout.write(0x0b); // Service Code List (lower byte)
  bout.write(0x01); // Service Code List (upper byte)
  bout.write(size); // Number of Blocks
  for (int i = 0; i < size; i++) { //Block Data
    bout.write(0x80); // Block List (uppder byte) / see Felica User Manual Section 4.3
    bout.write(i); // Block List (lower byte) Block Number
  }
  byte[] msg = bout.toByteArray();
  msg[0] = (byte) msg.length; // overwrite the data length
  return msg;
}

String parse(byte[] res) throws Exception 
{
  // res[0] = the data length
  // res[1] = 0x07 (Response Code)
  // res[2-9] = IDm
  // res[10,11] = Status Flag (0 means normal)
  if (res[10] != 0x00)
    throw new RuntimeException("Felica error.");

  // res[12] = Number of Blocks
  // res[13+n*16] Block Data
  int size = res[12];
  byte[] res2 = new byte [16];
  for (int i = 0; i < 16; i++) {
    res2[i] = res[13 + i];
  }
  return new String(res2, "UTF-8");
}