0次発行FeliCa LiteにNDEFを書き込む

まえがき

Android Advent Calendar 2012 (表)の8日目担当の@tomorrowkey です!
裏は @rukiadia さんです。
がんばります!

いきさつ

0次発行状態のFeliCa LiteにNDEFを書き込めるソフトウェアがなかったので、自分で作りました。
WindowsではPaSoRi+NDEFWriterで、1次発行状態にすると同時にNDEFを書き込むことができます。
NDEFを書き込むために1次発行は必須ではないので、0次発行の状態でNDEFを書き込めるようにしました。

某イベントで2,000枚のFeliCa LiteにSmartPosterを書き込む必要がありました。
当初はPaSoRi+NDEFWriterでやろうかと思ったのですが
1枚でも書き込むためにURLを入力したり、ダイアログのOKボタンを押したりと
オペレーションが煩雑だったのでアプリを作り、連続で書き込めるようにしたのです。

その時はただ同じ値をかければよかったので、マジックナンバーの嵐だったのですが
今回、値を動的に変えられるように、あとわりと綺麗なソースになるように書き直しました。
ライブラリは使っておらず、スクラッチしています。
まだまだ手を抜いてるところがたくさんあるので、なんとかしたいです。
てきとーですが、ここからは各所の解説など書きます。

アプリがFeliCa Liteに反応するようにする

use-permission

NFCを使うためにpermissionの設定が必要です。
/AndroidManifest.xml

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

NFCのIntentはNEW_TASKがついた状態で飛んできます。
いちいち新しいActivityが起動されてはうざいので
launchModeにsingleTaskもしくはsingleInstanceを指定して防ぎます。

android:launchMode="singleTask"
enableForegroundDispatch

NFCが反応したら優先して自分のアプリが起動されるようにforegroundDispatchという機能を使います。
アプリがフォアグラウンドに表示されている状態でNFCをフックしたいので、onResumeに処理を書きます。
あとでonPauseに解除するコードを書けばOKです。
/src/jp/tomorrowkey/android/felicalitewriter/WriteActivity.java

@Override
protected void onResume() {
  super.onResume();

  mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
  if (mNfcAdapter == null) {
    Toast.makeText(getApplicationContext(), "not found NFC feature", Toast.LENGTH_SHORT)
        .show();
    finish();
    return;
  }

  if (!mNfcAdapter.isEnabled()) {
    Toast.makeText(getApplicationContext(), "NFC feature is not available",
        Toast.LENGTH_SHORT).show();
    finish();
    return;
  }

  PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this,
      getClass()), 0);
  IntentFilter[] intentFilter = new IntentFilter[] {
    new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED),
  };
  String[][] techList = new String[][] {
    {
      android.nfc.tech.NfcF.class.getName()
    }
  };
  mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilter, techList);

}

pendingIntentにはNFCタグを検出した時に投げて欲しいIntentを入れてあげます。
IntentFilterとtechListには検出したいNFCタグの種類を指定します。
IntentFilterのActionには

  • ACTION_NDEF_DISCOVERED
    • NDEFフォーマットされたNFCタグ
  • ACTION_TECH_DISCOVERED
    • NDEFフォーマットされていないタグの中で、タグの種類指定ができる
  • ACTION_TAG_DISCOVERED
    • どんなタグでもいいから検出する

techListにはタグの種類を指定します。
今回はFeliCa Liteだけに対応したいので

  • ActionにACTION_TECH_DISCOVERED
  • techListにNfcF

を指定します。

disableForegroundDispatch

アプリがバックグラウンドに入ったときには、NFCフックしなくていいので
有効にしたforegroundDispatchを無効にします。

@Override
public void onPause() {
  super.onPause();

  mNfcAdapter.disableForegroundDispatch(this);
}

FeliCa の仕様について

ここまで作るとActivity#onNewIntent(:intent)にNFCタグの情報が飛んでくるようになります。
ExtraからTagを引っ張ってNDEFを書いたりするわけですが
中には書けないFeliCa Liteタグがあります。
それはNDEFフラグがたっていないものです。*1
通販等で購入したFeliCa Liteタグはたいていたっていない状態です(あたりまえですね)
つまり、NDEFフラグを立てればいいわけですが
Android SDKでは、FeliCa LiteのNFCフラグをたてる機能がついていません。
自分でポチポチたてる必要があります。
そのためにFeliCa Liteの仕様について知る必要があります。
がんばろう。

一般的なコマンドの体型

コマンド長 : 1 Byte
先頭に全体のコマンド長が入ります。
それはコマンド長を格納する1Byteも含みます。
よく忘れます。

コマンド : 1 Byte
コマンドを指定します。
FeliCa Liteの仕様書に載っているコマンドは以下のもののみです。

  • Polling
    • 0x00
  • Read Without Encryption
    • 0x06
  • Write Without Encryption
    • 0x08

FeliCa Standardにはもうすこしあります。

IDm : 8 Byte
コマンドによりますが、そのあとにはだいたいIDmが入ります。
IDmとはFeliCaタグひとつひとつで一意に識別できるIDのことです。
製造番号みたいなもので、同じ通信領域内に複数のFeliCaタグがあった際に
通信相手を選ぶために使われます。

書き込みコマンドについて

FeliCa Liteには以下のような制限があります。

  • 一度に書き込めるブロック数は1つまで
  • サービスコードは0x0009に固定
  • アクセスモードは000に固定
  • サービスコード順番は0000に固定

FeliCa Standardと比べれば考えることが少なくて楽です。
以上のことを踏まえて書き込みコマンドを実装すると以下のようになります。

/**
 * Write Without Encryptionコマンドを発行します<br>
 * FeliCa Liteなので、1度のコマンド発行で1ブロックだけ書き込めます
 * 
 * @param idm IDm
 * @param blockNumber ブロック番号
 * @param data 書き込みデータ
 * @return レスポンス
 * @throws TagLostException
 * @throws IOException
 */
public byte[] writeWithoutEncryption(byte[] idm, int blockNumber, byte[] data)
    throws TagLostException, IOException {
  if (idm == null || idm.length == 0)
    throw new IllegalArgumentException();

  ByteBuffer byteBuffer = ByteBuffer.allocate(31);

  // Write Without Encryption
  byteBuffer.put((byte)0x08);

  // IDm
  byteBuffer.put(idm);

  // サービス数
  // FeliCa Liteなので1に固定
  byteBuffer.put((byte)0x01);

  // サービスコード(リトルエンディアン)
  // 0x00 0x09
  byteBuffer.put((byte)0x09);
  byteBuffer.put((byte)0x00);

  // ブロックリスト
  // 長さ 2Byteなので1b
  // アクセスモード FeliCa Liteなので000bに固定
  // サービスコード順番 FeliCa Liteなので0000bに固定
  // ブロック番号 引数から指定
  byteBuffer.put((byte)0x80);
  byteBuffer.put((byte)blockNumber);

  // 書き込みデータ
  byteBuffer.put(data);

  byte[] command = byteBuffer.array();
  byte[] response = executeCommand(command);

  return response;
}

コマンド長はコマンド発行直前に付加するためはぶいてあります。

FeliCa Liteのメモリマップについて

これで書き込みコマンド組立は完成しました。
次にどこになにを書けばいいか調べます。
FeliCa Liteのメモリマップは以下のようになっています。
(これもFeliCa Liteの仕様書に書かれています)

S_PADと書かれた部分が、ユーザーブロックと呼ばれる場所で
ここにさまざまなデータを書き込みます。
そのほかの領域はいろいろな役割や機能があるので詳しくは仕様書を参考にしてください。
ユーザブロック以外にこんかい必要になるのが
MCブロック(メモリーコンフィグレーションブロック)です。

MCブロックのSYS_OP(System Option)に0x01を指定することで
FeliCa LiteがNDEF化されます。
通常は0x00です。

FeliCa LiteのNDEFフラグをたてる

さっそく書き込みコマンドを使ってNDEF化します。

// NDEF化します
byte[] data = new byte[] {
    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, (byte) 0x07, (byte) 0x00, (byte) 0x00, (byte) 0x00, 
    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, 
};
felicaLiteTag.writeWithoutEncryption(idm, 0x88, data);

MCブロックに書き込みたいので、ブロック番号に0x88を指定し
データに1ブロック分のデータを指定します。
一度の書き込みで1ブロックすべてを指定しなければなりません。
このブロックの中には一度特定の値を書き込むと、二度とその値を変更できない箇所があります。
ですので、気をつけて書き込みましょう。
本来であれば現在のブロックデータを読み込んでSYS_OPの部分だけ変更したほうがいいんだけど
手を抜きました。

NDEFデータを作る

タグのメタデータっぽいところはNDEFに対応できたから
今度はデータ本体を作ります。
AndroidNFC対応!だなんて謳ってるくらいだから、NDEFの各フォーマットビルダーくらい用意されてるよなーって
思ったんですが、ありません。
仕様書を読んで自分で作るか、どこかからひろってきましょう。
NDEF仕様書はNFC Forumにあります。
NDEFについては'NFC Data Exchange Format (NDEF) Technical Specification' という仕様書
RTDについては'Record Type Definition Technical Specifications' の各仕様書
を参考にしてください。

NFC Forum : Technical Specifications 
http://www.nfc-forum.org/specs/spec_list/

以前NDEFについて発表したときの資料はここから

避けては通れないバイナリ地獄 - NDEFってなんだろう - 
http://www.slideshare.net/tomorrowkey/ndef-13784268
避けては通れないバイナリ地獄 - NDEFってなんだろう - 
http://www.slideshare.net/tomorrowkey/ndef-13784268

ちなみに身近なところでNDEFデータを作るサンプルとしては
ApiDemosにRTD-Textを作るコードが入っています。

NDEFデータができたら書き込みます。
NDEFフラグを変更した時と同じ要領です。

NDEFヘッダを作成する

データができたところで、S_PAD(スクラッチパッド)に書き込みたいところですが
そのまま書いてもNDEFだと認識してくれません。
実はS_PAD0をAttribute Information Blockにしないといけないためです。
そのへんの仕様はNFC Forumに定義されています。
FeliCa系のタグはNFC ForumではType 3となっているので
NFC Forum Type 3 Tag Operation Specification
という仕様書を見てみると書いてあります。

NFC Forum : Technical Specifications 
http://www.nfc-forum.org/specs/spec_list/


  • Ver

バージョンを指定します。
1.0を表す0x10を指定します。

  • Nbr

タグ読み取り時に、一度に読めるブロック数を指定します。
FeliCa Liteなので0x04を指定します。

  • Nbw

タグ書き込み時に、一度に書き込めるブロック数を指定します。
FeliCa Liteなので0x01を指定します。

  • Nmaxb

NDEFデータとして使えるブロック数を指定します。
13ブロックなので0x00 0x0Dを指定します。

  • WriteF

データは一つのタグに収まるので、0x00を指定します。

  • RW Flag

今後も書き込みできる状態なので、0x01を指定します。

  • Ln

NDEFデータの長さを指定します。
実際のデータ長から設定されます。

  • Checksum

チェックサムです。
ブロック0のすべてを加算した値を設定します。

private byte[] createNdefHeader(int ndefLength) {
  ByteBuffer buffer = ByteBuffer.allocate(16);

  // Ver
  buffer.put((byte)0x10);

  // Nbr
  // Read Without Encrypitonで一度に読めるブロック数を指定します
  // FeliCa Liteなので、一度に4ブロック読み込める
  buffer.put((byte)0x04);

  // Nbw
  // Write Without Encryptionで一度に書き込めるブロック数を指定します
  // FeliCa Liteなので、一度に1ブロック書き込める
  buffer.put((byte)0x01);

  // Nmaxb
  // NDEFとして使用できるブロック数
  // FeliCa Liteなので、データ領域は13ブロックまで
  buffer.put((byte)0x00);
  buffer.put((byte)0x0d);

  // unused
  buffer.put((byte)0x00);
  buffer.put((byte)0x00);
  buffer.put((byte)0x00);
  buffer.put((byte)0x00);

  // WriteF
  // 一枚で完結しているので、0x00
  buffer.put((byte)0x00);

  // RW Flag
  // Read Writeなので0x01
  buffer.put((byte)0x01);

  // Ln
  // NDEFデータの長さを指定します
  buffer.put((byte)((ndefLength >>> 16) & 0xff));
  buffer.put((byte)((ndefLength >>> 8) & 0xff));
  buffer.put((byte)(ndefLength & 0xff));

  // Checksum
  // チェックサムを指定します
  buffer.put(checksum(buffer.array()));

  return buffer.array();
}

/**
 * チェックサムを作成します<br>
 * すべてのバイト配列の合計を計算します
 * 
 * @param byteArray
 * @return
 */
private byte[] checksum(byte[] byteArray) {
  int sum = 0;
  for (byte b : byteArray) {
    sum += b & 0xff;
  }
  return new byte[] {
    (byte)((sum >>> 8) & 0xff), (byte)(sum & 0xff)
  };
}
NDEFデータを書き込む

これで、すべての材料は揃いました。
ユーザーブロックの0から順にデータを書き込みます。

/**
 * NdefMessageを書き込みます
 * 
 * @param idm IDm
 * @param ndefMessage NDEF
 * @throws SizeOverflowException NdefMessageのサイズが大きすぎる場合に発生します
 * @throws TagLostException
 * @throws IOException
 */
public void writeNdefMessage(byte[] idm, NdefMessage ndefMessage) throws SizeOverflowException,
    TagLostException, IOException {
  if (idm == null || idm.length == 0)
    throw new IllegalArgumentException();
  if (ndefMessage == null)
    throw new IllegalArgumentException();

  byte[][] datas = mappingBlock(ndefMessage);

  for (int blockNumber = 0; blockNumber <= 13; blockNumber++) {
    // FIXME レスポンスを握りつぶしているので、どうにかする
    writeWithoutEncryption(idm, blockNumber, datas[blockNumber]);
  }
}

/**
 * NdefMessageからFeliCa Liteの各ブロックにマッピングします
 * 
 * @param ndefMessage
 * @return
 * @throws SizeOverflowException
 * @throws IOException
 */
private byte[][] mappingBlock(NdefMessage ndefMessage) throws SizeOverflowException,
    IOException {
  byte[] ndefMessageBytes = ndefMessage.toByteArray();
  int ndefMessageBytesLength = ndefMessageBytes.length;
  int blockCount = (int)Math.ceil(ndefMessageBytesLength / 16.0);
  if (blockCount > 13)
    throw new SizeOverflowException(ndefMessageBytesLength, 16 * 13);

  byte[][] datas = new byte[14][16];
  datas[0] = createNdefHeader(ndefMessageBytesLength);

  ByteArrayInputStream inputStream = new ByteArrayInputStream(ndefMessageBytes);
  try {
    inputStream = new ByteArrayInputStream(ndefMessageBytes);
    int readLength;
    for (int i = 1; i < datas.length; i++) {
      readLength = inputStream.read(datas[i]);
      if (readLength == -1)
        break;
    }
  } finally {
    try {
      if (inputStream != null)
        inputStream.close();
    } catch (IOException e) {
      // ignore
    }
  }

  return datas;
}

完成

これでNDEFを書きこめるようになりました。
たのしいですね。

ソースコードはここに晒しておきます。
https://github.com/tomorrowkey/FeliCaLiteWriter

最後に

明日はNexus 7がなかなか届かないことで有名な@sekitoba さんと
@sugimotoak さんです。
みんながんばれー

*1:ユーザーエリアがReadOnlyに変更されている場合もあります