Help us understand the problem. What is going on with this article?

[PASMO] FeliCa から情報を吸い出してみる - FeliCaの実装編 [Android][Kotlin]

More than 1 year has passed since last update.

Suica/PASMO の券面に印字された情報が NFC の機能で取得できるのか調査した時の Android での実装をまとめたもの です。
以前書いた [PASMO] FeliCa から情報を吸い出してみる - FeliCaの仕様編 [Android][Kotlin] の実装編ということです。

私は PASMO ユーザなので、PASMO での調査ログになりますことをご了承ください。

次のことをまとめようと思います。

  1. Android で FeliCa を扱うときに出てくるクラス達の紹介
  2. 実装について
  3. 悩みどころ
  4. 開発時に気をつけること

※ NFC のライブラリはいくつかあって、仕様編でも参考元リンクとして貼っていたりしますが、それらは使っていません。Android標準オンリーです。
※ Suica/PASMO などの鉄道系カードの内部仕様は 非公開 となっているようで、公式な情報はありません。システムコードやサービスコードなどの固定値が出てきますが、有志が集めた情報を元に実装しています。

Android で FeliCa を扱うときに出てくるクラス達の紹介

FeliCa 用というか、NFC 用のクラスは次の3つです。

それぞれの解説をします。

android.nfc.NfcAdapter

NFCデバイス(Suica だとかのカード)の検出を開始/停止するためのクラスです。
ハードウェアの制御のインターフェースとなるので、インスタンスを取得するために Context を要求します。
また、インスタンス化せずとも AndroidManifest.xml にて Activity なり Receiver を定義して intent-filter を設定することで代替することもできます。

使い分けとしては、アプリがフォアグラウンドの状態で NFCデバイスを検出したい時 に、このクラスをインスタンス化します。
アプリがバックグラウンドの状態で NFCデバイスを検出したい時 は、AndroidManifest.xml への定義をします。

いずれも、目的は Tag オブジェクトを取得することです。

android.nfc.Tag

検出した NFCデバイスを表すモデルです。
このオブジェクトを利用して、NFCデバイスとやりとりをすることになります。

この Tag オブジェクトは ID を持っています。
Android のリファレンスの getId() の説明に、次の記載があります。

Get the Tag Identifier (if it has one).

The tag identifier is a low level serial number, used for anti-collision and identification.

Most tags have a stable unique identifier (UID), but some tags will generate a random ID every time they are discovered (RID), and there are some tags with no ID at all (the byte array will be zero-sized).

The size and format of an ID is specific to the RF technology used by the tag.

This function retrieves the ID as determined at discovery time, and does not perform any further RF communication or block.

真ん中あたりの文がとても不安を感じさせます。
Google翻訳にかけると次のようになります。

ほとんどのタグは安定したユニークな識別子(UID)を持っていますが、いくつかのタグは発見されるたびにランダムなIDを生成し、IDのないタグもあります(バイト配列はゼロサイズになります)。

これに関しては、NFC-Developer.com: FeliCa IDmとは? にあるように、公共の電子マネーサービスではこのIDmやUIDは利用されていない らしいです。おそらくこれらのことかと思います。
※ 実際に利用されていないカードが手元になさそうで、詳細を追えませんでした・・・。

android.nfc.tech.NfcF

このクラスは NFC-F 規格の NFCデバイスの詳細を持つクラスです。
検出対象の NFCデバイスの規格をフィルタリングする際には、このクラスの FQDN を使用します。

このクラスをインスタンス化するには、前述の Tag のインスタンスが必要です。
NfcF のインスタンスを利用し、NFCデバイスとの 通信の開始/終了リクエストの送信 をします。

実装について

流れとしては、次のようになります。

  1. Tag オブジェクトを取得する
  2. NFC デバイスの検出を受け取る
    1. Tag から NfcF クラスのインスタンスを生成する
    2. NfcF オブジェクトを通して NFCデバイスと接続する
    3. コマンドを実行し、欲しい情報を取得する
    4. NfcF オブジェクトを通して NFCデバイスとの接続を切断する

まず 2-3 の詳細以外の大枠部分を見ていきます。

Tag オブジェクトを取得する

※ わたしは NFCデバイスの検出をフォアグラウンドで行う実装をしたので、バックグラウンドで検知したい人は It’s now or never:【Android】NFCを使ってみる① (読み込み処理) などを参照してみてください。

Tag オブジェクトの取得 は、NFCデバイスの情報を取得する入り口です。
フォアグラウンドで NFCデバイスを検出する場合は、まず NfcAdapter をインスタンス化します。

class MainActivity : AppCompatActivity() {
  private val nfcAdapter by lazy { NfcAdapter.getDefaultAdapter(this) }
}

NfcAdapter のフォアグラウンド検出開始を実施します。
検出開始には、検出時に実行される PendingIntent、検出イベントを表す IntentFilter が必要です。

// PendingIntent 起動時に実行される intent の生成と PendingIntent のインスタンス化をする。
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val feliCaPendingIntent = PendingIntent.getActivity(this, 0, intent, 0)

// NfcAdapter.ACTION_NDEF_DISCOVERED をアクション名とするフィルタを生成する。
val filter = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).apply { this.addDataType("*/*") }

// NfcAdapter の検出開始を実行する。
nfcAdapter.enableForegroundDispatch(this, feliCaPendingIntent, arrayOf(filter), arrayOf(NFC_TYPES))

NFC_TYPES は検出対象の NFCデバイスの規格を表すクラスの FQDN の配列です。
固定値なので、次のように定義しました。
検出対象が他にもある場合は、配列の要素として NfcA なり NfcB なり NfcV なりのクラス名を入れてあげましょう。

companion object {
  val NFC_TYPES = arrayOf(NfcF::class.java.name)
}

NFC デバイスの検出を受け取る

検出イベントの受け口は onNewIntent(...) です。

override fun onNewIntent(intent: Intent?) {
  if (intent == null) {
    return
  }
  // intent から Tag オブジェクトを取り出す。
  val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return
  Log.d("FeliCaSample", "tag: '$tag', id: '${tag.id.joinToString(" ")}'")

  // NFCデバイスとのやり取りを実施する。
  val result = NfcFReader().read(tag)
  Toast.makeText(this, "tag: '$tag', id: '${tag.id.joinToString(" ")}'", Toast.LENGTH_SHORT).show()
}

NfcFReader は独自クラスです。
read(tag) の中身は次のようになっています。

詳細は次に続く解説を参照ください。

class NfcFReader() {
  /**
   * NFCデバイスと接続し、通信をする。
   *
   * @param tag 検出した NFCデバイス
   * @return 処理に成功した場合は Read Without Encryption コマンドの結果オブジェクトを、失敗した場合は null を返す。
   */
  fun read(tag: Tag): ReadWithoutEncryptionResponse? {
    // NfcF オブジェクトの取得をする。
    val nfc = NfcF.get(tag)
    try {
      // NFCデバイスに接続する。
      nfc.connect()

      // もろもろコマンドを実行した結果を取得する。
      val result = read(nfc)
      Log.d("FeliCaSample", "read without encryption: '$result'")

      if (result != null) {
        // 結果の肝である block をログに出力する。
        result.blocks.forEachIndexed { i, bytes -> Log.d("FeliCaSample", "block[$i] '${bytes.joinToString(" ")}'") }
      }
      // NFCデバイスとの接続を切断する。
      nfc.close()

      return result
    } catch (e: Exception) {
      Log.e("FeliCaSample", "cannot read nfc. '$e'")
      if (nfc.isConnected) {
        nfc.close()
      }
    }
    return null
  }

Tag から NfcF クラスのインスタンスを生成する

NFCデバイスとやり取りをしていきます。
遣り取りをするクラスとして NfcFReader というクラスを作成しました。
実質、ただの NFCデバイスとやり取りするだけのクラスです。

NfcF のインスタンスの取得は、前述の NfcFReader.read(Tag) の中の次の部分です。

// NfcF オブジェクトの取得をする。
val nfc = NfcF.get(tag)

NfcF オブジェクトを通して NFCデバイスと接続する

NfcF オブジェクトを取得できたので、NFCデバイスに接続します。
前述の NfcFReader.read(Tag) の中の次の部分です。

// NFCデバイスに接続する。
nfc.connect()

コマンドを実行し、欲しい情報を取得する

NFCデバイスと接続できたので、中身を読み取ります。
前述の NfcFReader.read(Tag) の中の次の部分です。

// もろもろコマンドを実行した結果を取得する。
val result = read(nfc)

肝の部分なので、後述します。

NfcF オブジェクトを通して NFCデバイスとの接続を切断する

やることが終わったらお片付けをします。
前述の NfcFReader.read(Tag) の中の次の部分です。

// NFCデバイスとの接続を切断する。
nfc.close()

肝の部分 - コマンドを実行し、欲しい情報を取得する

肝の部分です。
情報を取得しましょう。
入り口はここです。

// もろもろコマンドを実行した結果を取得する。
val result = read(nfc)
Log.d("FeliCaSample", "read without encryption: '$result'")

中身の記述量は多いですが、やることは3つのステップだけです。

  1. Polling コマンドを実施して IDm を取得する ※ ついでにシステムコードの検証を行う
  2. Request Service コマンドを実施してサービスコードの正当性を確認する
  3. Read Without Encryption コマンドを実施して システムコード, サービスコードに対応する情報を取得する

各コマンドを送信する部分はすべて、次の3ステップです。

  1. リクエストパケットを生成する
  2. NfcF オブジェクトの transceive(...) にリクエストパケットを渡す
  3. transceive(...) のレスポンスを解釈する

中身をまず見ましょう。

/**
 * NFCデバイスの情報を取得する。
 *
 * @param nfc NFC-F 規格用のオブジェクト
 * @return 処理に成功した場合は Read Without Encryption コマンドの結果オブジェクトを、失敗した場合は null を返す。
 */
private fun read(nfc: NfcF): ReadWithoutEncryptionResponse {
  // System 1のシステムコード -> 0x0003 (SUICA/PASMO などの鉄道系)
  val targetSystemCode = byteArrayOf(0x00.toByte(), 0x03.toByte())

  // [1]: Polling コマンドオブジェクトのリクエストパケットを nfc に送信し、Polling の結果を受け取る。
  val pollingCommand = PollingCommand(targetSystemCode)
  val pollingRequest = pollingCommand.requestPacket()
  val rawPollingResponse = nfc.transceive(pollingRequest)
  val pollingResponse = PollingResponse(rawPollingResponse)

  // Polling で得られた IDm を取得する。
  val targetIDm = pollingResponse.IDm()

  // 実行したいサービスコードを生成する。
  val serviceCode = byteArrayOf(0x00.toByte(), 0x8B.toByte())

  // [2]: Request Service コマンドオブジェクトのリクエストパケットを nfc に送信し、Request Service の結果を受け取る。
  val requestServiceCommand = RequestServiceCommand(targetIDm, serviceCode)
  val requestServiceRequest = requestServiceCommand.requestPacket()
  val rawRequestServiceResponse = nfc.transceive(requestServiceRequest)
  val requestServiceResponse = RequestServiceResponse(rawRequestServiceResponse)

  // [3]: Read Without Encryption コマンドオブジェクトのリクエストパケットを nfc に送信し、Read Without Encryption の結果を受け取る。
  val readWithoutEncryptionCommand = ReadWithoutEncryptionCommand(IDm = targetIDm, serviceCode = serviceCode, blocks = arrayOf(BlockListElement2(BlockListElement.AccessMode.toNotParseService, 0, 0)))
  val readWithoutEncryptionRequest = readWithoutEncryptionCommand.requestPacket()
  val rawReadWithoutEncryptionResponse = nfc.transceive(readWithoutEncryptionRequest)
  val readWithouEncryptionResponse = ReadWithoutEncryptionResponse(rawReadWithoutEncryptionResponse)

  return readWithouEncryptionResponse
}

それぞれのステップを詳しく見ましょう。

※ リクエストクラスの親である NfcCommand は、独自のインターフェースで val commandCode: Bytefun requestPacket(): ByteArray が定義されています。小さいので割愛します。

※ レスポンスクラスの親である NfcResponse はコンストラクタで ByteArray を受け取り、abstract な getter を3つ abstract fun responseSize(): Int , abstract fun responseCode(): Byte, abstract fun IDm(): ByteArray を定義しています。小さいので全貌は割愛します。

Polling コマンドを実施して IDm を取得する

Polling コマンドを実施するのは、前述の NfcFReader.read(NfcF) の中の次の部分です。

// [1]: Polling コマンドオブジェクトのリクエストパケットを nfc に送信し、Polling の結果を受け取る。
val pollingCommand = PollingCommand(targetSystemCode)
val pollingRequest = pollingCommand.requestPacket()
val rawPollingResponse = nfc.transceive(pollingRequest)
val pollingResponse = PollingResponse(rawPollingResponse)

リクエストパケットを生成する

Polling コマンドを実施する際に必要な値は次の1つです。

  • システムコード

システムコードはシステムを特定する値です。
Suica/PASMO などの鉄道系のカードは 00 03 だそうです。

Polling コマンドのリクエストパケットを生成している PollingCommand クラスは次のようなっています。

data class PollingCommand(private val systemCode: ByteArray, private val request: Request = PollingCommand.Request.systemCode) : NfcCommand {
  /** Polling コマンドで取得する情報の列挙体。 */
  enum class Request(val value: Byte) {
    none(0x00), systemCode(0x01), communicationAbility(0x02)
  }

  /** Polling コマンドのコマンドコードは 0x00 */
  override val commandCode: Byte
    get() = 0x00

  /** タイムスロットは 0F 固定とする(ほんとはもっとちゃんと指定できる。仕様編参照) */
  val timeSlot: Byte
    get() = 0x0f

  /** リクエストコードのバイト値 */
  val requestCode: Byte
    get() = request.value

  /** リクエストパケットを取得する。 */
  override fun requestPacket(): ByteArray {
    return ByteArray(6).apply {
      var i = 0
      this[i++] = 0x06              // [0] 最初はリクエストパケットのサイズが入る。6byte固定。
      this[i++] = commandCode       // [1] コマンドコードが入る。
      this[i++] = systemCode[0]     // [2] システムコードの先頭byteが入る。
      this[i++] = systemCode[1]     // [3] システムコードの末尾byteが入る。
      this[i++] = requestCode       // [4] リクエストコードが入る。
      this[i++] = timeSlot          // [5] タイムスロットが入る。
    }
  }
}

解説が必要なところは特にないかと思います。
このリクエストパケットを nfc.transceive(...) に渡すと、レスポンスを受け取れます。
レスポンスの解釈をしている PollingResponse クラスは次のようになっています。

class PollingResponse(response: ByteArray) : NfcResponse(response) {
  /** レスポンスのサイズを取得する。 */
  override fun responseSize(): Int {
    return response[0].toInt()
  }

  /** レスポンスコードの取得をする。 */
  override fun responseCode(): Byte {
    return response[1]
  }

  /** IDm を取得する。 */
  override fun IDm(): ByteArray {
    return response.copyOfRange(2, 10)
  }
}

こちらも特に解説は必要ないかと思います。
受け取った ByteArray をそれぞれ getter でラップしているだけです。

Request Service コマンドを実施してサービスコードの正当性を確認する

Request Service コマンドを実施するのは、前述の NfcFReader.read(NfcF) の中の次の部分です。

// [2]: Request Service コマンドオブジェクトのリクエストパケットを nfc に送信し、Request Service の結果を受け取る。
val requestServiceCommand = RequestServiceCommand(targetIDm, serviceCode)
val requestServiceRequest = requestServiceCommand.requestPacket()
val rawRequestServiceResponse = nfc.transceive(requestServiceRequest)
val requestServiceResponse = RequestServiceResponse(rawRequestServiceResponse)

リクエストパケットを生成する

Request Service コマンドを実施する際に必要な値は次の2つです。

  • IDm
  • サービスコード

サービスコードはシステムが提供する サービス です。
例えば、今回の例のサービスコード 00 8B は、カード情報の取得 を表します。

Request Service コマンドのリクエストパケットを生成している RequestServiceCommand クラスは次のようなっています。

data class RequestServiceCommand(private val IDm: ByteArray, private val nodeCodeList: Array<ByteArray>) : NfcCommand {
  /** ノードコードが1つの場合のためのセカンダリコンストラクタ。 */
  constructor(IDm: ByteArray, nodeCode: ByteArray): this(IDm, arrayOf(nodeCode))

  /** Request Service コマンドのコマンドコードは 0x02 */
  override val commandCode: Byte
    get() = 0x2

  /** ノードコードの件数 */
  private val numberOfNodes = (nodeCodeList.size).toByte()

  /** パケットのサイズ */
  private val packetSize = (11 + nodeCodeList.size * 2)

  /** リクエストパケットを取得する。 */
  override fun requestPacket(): ByteArray {
    return ByteArray(packetSize).apply {
      var i = 0
      this[i++] = packetSize.toByte()       // [0] 最初はリクエストパケットのサイズが入る。
      this[i++] = commandCode               // [1] コマンドコードが入る。
      IDm.forEach { this[i++] = it }        // [2..10] IDm (8byte) が入る。
      this[i++] = numberOfNodes             // [11] ノードの数が入る。
      nodeCodeList.forEach {
        it.forEachIndexed { index, byte ->
          if ((index % 2) == 0) {           // [12..] ノードコード (2byte) が入る。
            this[i + 1] = byte              //        ノードコードはリトルエンディアンなので
          } else {                          //        2byte が反転するように格納する。
            this[i - 1] = byte
          }
          i++
        }
      }
    }
  }
}

リクエストパケットの生成のノードコード一覧の部分はちょっとめんどくさいです。
ノードコードリストの要素であるノードコードは 2byte で、リクエストに使用する際はリトルエンディアンにする必要があります。
このリクエストパケットを nfc.transceive(...) に渡すと、レスポンスを受け取れます。
レスポンスの解釈をしている RequestServiceResponse クラスは次のようになっています。

class RequestServiceResponse(response: ByteArray) : NfcResponse(response) {
  /** ノードコードに対応するノードの鍵バージョンモデル */
  data class NodeKeyVersion(private val values: ByteArray) {
    val value: ByteArray
      get() {
        return ByteArray(2).apply {
          this[0] = values[1]
          this[1] = values[0]
        }
      }
  }

  /** レスポンスのサイズを取得する。 */
  override fun responseSize(): Int {
    return response[0].toInt()
  }

  /** レスポンスコードを取得する。 */
  override fun responseCode(): Byte {
    return response[1]
  }

  /** IDm を取得する。 */
  override fun IDm(): ByteArray {
    return response.copyOfRange(2, 10)
  }

  /** ノードの数を取得する。 */
  fun numberOfNodes(): Int {
    return response[11].toInt()
  }

  /** ノードの鍵バージョンの配列を取得する。 */
  fun nodeKeyVersions(): Array<NodeKeyVersion> {
    var i = 0
    val results = arrayListOf<NodeKeyVersion>()
    val rawNodeKeyVersions = response.copyOfRange(12, response.size)
    while (i < numberOfNodes()) {
      results += NodeKeyVersion(ByteArray(2).apply {
        this[0] = rawNodeKeyVersions[i * 2]
        this[1] = rawNodeKeyVersions[(i * 2) + 1]
      })
    }
    return results.toTypedArray()
  }
}

頑張って実装しましたが、複合したりはしないので鍵バージョンは使っていません。
例外が出ず、IDm が取得できればサービスコードの正当性検証は完了です。

Read Without Encryption コマンドを実施して システムコード, サービスコードに対応する情報を取得する

Read Without Encryption コマンドを実施するのは、前述の NfcFReader.read(NfcF) の中の次の部分です。

// [3]: Read Without Encryption コマンドオブジェクトのリクエストパケットを nfc に送信し、Read Without Encryption の結果を受け取る。
val readWithoutEncryptionCommand = ReadWithoutEncryptionCommand(IDm = targetIDm, serviceCode = serviceCode, blocks = arrayOf(BlockListElement2(BlockListElement.AccessMode.toNotParseService, 0, 0)))
val readWithoutEncryptionRequest = readWithoutEncryptionCommand.requestPacket()
val rawReadWithoutEncryptionResponse = nfc.transceive(readWithoutEncryptionRequest)
val readWithouEncryptionResponse = ReadWithoutEncryptionResponse(rawReadWithoutEncryptionResponse)

リクエストパケットを生成する

Read Without Encryption コマンドを実施する際に必要な値は次の3つです。

  • IDm
  • サービスコード
  • 読み取り対象のブロックの配列

ブロックの要素として BlockListElement を親クラスとする BlockListElement2, `BlockListElement3 を実装しました。
後述します。

Read Without Encryption コマンドのリクエストパケットを生成している ReadWithoutEncryptionCommand クラスは次のようなっています。

data class ReadWithoutEncryptionCommand(private val IDm: ByteArray, private val serviceList: Array<ByteArray>, val blocks: Array<BlockListElement>) : NfcCommand {
  /** サービスコードが 1種類の場合のためのセカンダリコンストラクタ。 */
  constructor(IDm: ByteArray, serviceCode: ByteArray, blocks: Array<BlockListElement>): this(IDm, arrayOf(serviceCode), blocks)

  /** Read Without Encryption コマンドのコマンドコードは 0x06 */
  override val commandCode: Byte
    get() = 0x06.toByte()

  /** サービスの数 */
  private val numberOfServices = serviceList.size

  /** ブロックの数 */
  private val numberOfBlocks: Int = blocks.size

  /** ブロックリスト全体の byte 数 */
  private val blockSize = blocks.map { it.size() }.reduce { a, b -> a + b }

  /** パケットサイズ */
  private val packetSize = 13 + (numberOfServices * 2) + blockSize

  /** リクエストパケットを取得する。 */
  override fun requestPacket(): ByteArray {
    return ByteArray(packetSize).apply {
      var i = 0
      this[i++] = packetSize.toByte()       // [0] 最初はリクエストパケットのサイズが入る。
      this[i++] = commandCode               // [1] コマンドコードが入る。
      IDm.forEach { this[i++] = it }        // [2..10] IDm (8byte) が入る。
      this[i++] = numberOfServices.toByte() // [11] サービスコードリストの数が入る。
      serviceList.forEach {
        it.forEachIndexed { index, byte ->  // [12..] サービスコード (2byte) が入る。
          if ((index % 2) == 0) {           //        サービスコードはリトルエンディアンなので
            this[i + 1] = byte              //        2byte が反転するように格納する。
          } else {
            this[i - 1] = byte
          }
          i++
        }
      }
      this[i++] = numberOfBlocks.toByte()    // [12 + 2 * numberOfServices + 1] ブロックリストの数が入る。
      blocks.forEach { it.toByteArray().forEach { this[i++] = it } }
                                             // [...] ブロックリストエレメントのパケットが順繰り入る。
    }
  }
}

サービスコードリストには、取得したい情報のサービスコードを入れます。
今回の実装では 00 8B(カード情報の取得)のみを対象としました。
カード情報の取得で必要なブロックは、2バイトのエレメント 1つです。

ブロックリストエレメントは次のように実装しました。
親クラスとなる abstract なブロックリストエレメントクラスに最初の 1byte を生成する処理を実装し、パケット全体を返すメソッド、ブロックのサイズを返すメソッドを定義しました。
ちなみに、アクセスモードは基本 0 で良いのかなと思います。
パースサービスってなんだ?という方は 仕様編 を参照ください。(私もよくわかってないです。)

abstract class BlockListElement(val accessMode: AccessMode, val serviceCodeIndex: Int, val number: Int) {
  /** アクセスモードの列挙体 */
  enum class AccessMode(val value: Int) {
    toNotParseService(0), toParseService(1)
  }

  /** ブロックリストエレメントの最初の byte を取得する。 */
  fun firstByte(): Byte {
    // 1byte
    //    [0] エレメントのサイズ (1bit) 0: 2byteエレメント, 1: 3byteエレメント
    //    [1] アクセスモード (1bit)
    //    [2..7] エレメントが対象とするサービスコードのサービスコードリスト内の番号 (6bit)
    //    x 0 0 0 0 0 0 0 <- エレメントのサイズ
    //    0 x 0 0 0 0 0 0 <- アクセスモード
    //  & 0 0 x x x x x x <- サービスコードリスト内の順番
    return ((0 shl 7) and (accessMode.value shl 6) and (serviceCodeIndex shl 3)).toByte()
  }
  abstract fun toByteArray(): ByteArray
  abstract fun size(): Int
}

2byteのブロックリストエレメントは次のようになります。

class BlockListElement2(accessMode: AccessMode, serviceCodeIndex: Int, number: Int) : BlockListElement(accessMode, serviceCodeIndex, number) {
  /** エレメントのパケットを取得する。 */
  override fun toByteArray(): ByteArray {
    return ByteArray(2).apply {
      this[0] = firstByte()
      this[1] = number.toByte()
    }
  }

  /** サイズを取得する。 */
  override fun size(): Int {
    return 2
  }
}

3byteのブロックリストエレメントは次のようになります。
この場合のパケットの構成に関しては、number を 2byte にすればよいだけなのですが、直近で利用する必要がなかったので手を抜きました。

class BlockListElement3(accessMode: AccessMode, serviceCodeIndex: Int, number: Int) : BlockListElement(accessMode, serviceCodeIndex, number) {
  /** エレメントのパケットを取得する。 */
  override fun toByteArray(): ByteArray {
    return ByteArray(3).apply {
      this[0] = firstByte()

      // FIXME: こうじゃない
      this[2] = number.toByte()
    }
  }

  /** サイズを取得する。 */
  override fun size(): Int {
    return 3
  }
}

ReadWithoutEncryptionCommand のリクエストパケットを nfc.transceive(...) に渡すと、レスポンスを受け取れます。
レスポンスの解釈をしている ReadWithoutEncryptionResponse クラスは次のようになっています。

class ReadWithoutEncryptionResponse : NfcResponse {
  /** 指定のサービスコードの情報が格納されるブロックリスト 16byte/ブロック */
  val blocks: Array<ByteArray>

  constructor(response: ByteArray): super(response) {
    // コンストラクタでブロックをパースする。
    blocks = blocks()
  }

  /** 処理の成否 */
  val succeeded: Boolean
    get() = statusFlag1().toInt() == 0x00 && statusFlag2().toInt() == 0x00

  /** レスポンスのサイズを取得する。 */
  override fun responseSize(): Int {
    return response[0].toInt()
  }

  /** レスポンスコードを取得する。 */
  override fun responseCode(): Byte {
    return response[1]
  }

  /** IDm を取得する。 */
  override fun IDm(): ByteArray {
    return response.copyOfRange(2, 10)
  }

  /** ステータスフラグ1 を取得する。詳細は仕様編参照。 */
  fun statusFlag1(): Byte {
    return response[10]
  }

  /** ステータスフラグ2 を取得する。詳細は仕様編参照。 */
  fun statusFlag2(): Byte {
    return response[11]
  }

  /** ブロックの数を取得する。 */
  fun numberOfBlocks(): Int {
    return response[12].toInt()
  }

  private fun blocks(): Array<ByteArray> {
    var i = 0
    val results = arrayListOf<ByteArray>()
    val raw = response.copyOfRange(13, response.size)
    while (i < numberOfBlocks()) {
      // 16byte 単位で配列に格納していく。
      results += raw.copyOfRange(i * 16, i * 16 + 16)
      i++
    }
    return results.toTypedArray()
  }
}

ステータスフラグで、処理の成否を判定できます。
1, 2ともに 00 であれば成功です。
失敗の場合は、失敗した箇所(バイト列番号)が入ります。

ブロックの解釈の仕方は、サービスに依存します。
実装時は、jennychan.web.fc2.com: システムコード/サービスコード一覧(NSのみ) を参照しました。

わたしの PASMO で実行した際のブロックは次のようになりました。

0  0  0  0  0  0  0  0 32  0  0 94  3  0  4 -39

区分けをわかりやすくすると [[00 00 00 00 00 00 00 00] [20] [00 00] [5E 03] [00] [04 27]] となります。
各部位の解説をすると次のようになるそうです。

解説
[00 00 00 00 00 00 00 00] 不明
[20] カード種別 (4bit), 使用地域 (4bit)
[00 00] 不明
[5E 03] 残額 (LE) 03 5E -> 862円 ※ 翌日出勤時に改札通るとたしかにこの額でした。
[00] 不明
[04 27] 取引通番

カード種別は上の参照したサイトで 20 だと SUICA/PASMO であるとの記載がありました。

悩みどころ

サンプルの開発をし、動作確認をしていて、よく出くわす次のエラーがありました。

android.nfc.TagLostException: Tag was lost

ググると、次のリンクのように解説してくださっている方がいます。

たしかに、これらの理由で発生することもありましたが、単純にスキャン中に NFCデバイスとスマホがずれると発生することがあるのです。
いまいち解決方法も、発生するタイミングも読めないので気持ち悪いです。
改善策はないものかしら・・・。

開発時に気をつけること

開発時は Galaxy 6s Edge を使用していました。
以下の気をつけることは端末に依存するものなのかもしれないです。

  • Android端末の設定で、FeliCa読み取りを有効にする
  • Android端末に標準で入っている FeliCaリーダー的なアプリに処理を奪われる

設定からアプリケーションを無効にすることで、独自アプリが処理をすすめることが出来るようになります。

  • カードに接続してデバッグしている最中でプロセスを切るとスマホが NFCデバイスを検知しなくなる現象

スマホを再起動すると、また元気に検知してくれるのですが・・・。
ちゃんと、NfcAdapter の disconnect() を通してから、プロセスを切るようにしましょう。
めんどくさいですが。

YasuakiNakazawa
iOS(ObjC, Swift), Android(Java, Kotlin), Rails、インフラ(CentOS、Ubuntu)一通りおさわりします。 一応、AWSとかラズパイとかFlutterとかもやったりします。 ちょっとだけCPPとかGolangとかVueJSとかTypeScriptとかもやったりします。 上から下までやれる人にワタシハナリタイ
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした