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

FeliCa システムコードの切り替えは Polling コマンドのみで【iOS 13 Core NFC】

さて、この Core NFC Advent Calendar 2019 で、私は以下の FeliCa カードの読み取りに関する記事を執筆、公開しました。

そして FeliCa の特徴として、1つの物理カードの中に複数のシステムを持つことができる、というものがあります。これにより、1枚のカードで複数のサービスが利用可能になります。

FeliCa が複数のシステムを持つことができることをご存知の場合は本題までスキップ

1枚のカードに1つのシステムがあるパターン

例えば、Suica、PASMO、ICOCA などに代表される交通系ICは 0x0003 という FeliCa システムコードが主に使われています。
そして、楽天Edy、nanaco、WAON は 0xFE00 という FeliCa システムコードを使っています。

それぞれ、その機能しか持たないカードの場合は、だいたい1つの FeliCa システムコードを持っていることになります。

※厳密に言えば、例えば Suica や PASMO は 0x0003 の他にも FeliCa システムコードを持っています。また、カードの発行時期によって持っている FeliCa システムコードの数が異なっています。ここでは各サービスを利用するときに使われている FeliCa システムコードについて説明したため、このような記述としています。

1枚のカードに複数のシステムがあるパターン

では、冒頭に挙げた「1つの物理カードの中に複数のシステムを持つことができる」というのはどういうことでしょうか。
もっともわかりやすい例は「おサイフケータイ」かなと思います。
おサイフケータイ機能では1つの端末に複数のアプリケーションを入れることができ、例えばモバイル Suica と WAON といった組み合わせが共存できます。
そして、改札にかざすときはモバイル Suica が反応し、WAON 読み取り端末にかざすと WAON が反応します。

物理カードとして私が「これは変態だ…」と思ったカードは、広島銀行が発行する「ひろぎんPASPY」です。このカードは1枚に

  • PASPY(広島地域の交通系電子マネー)
  • ちゅーピーくらぶ会員証(中国新聞読者向けの会員証)
  • QUICPay(ポストペイ型電子マネー)
  • HIROCA(広島地域の電子マネー)

の4種類が使えるようになっており、もはや正気の沙汰ではありません。最高です。
以前は QUICPay ではなく Visa Touch を搭載したものもあったようです。

※これは完全に余談ですが、「Visa Touch」は FeliCa が用いられており、現行の「Visaのタッチ決済(旧 Visa payWave)」とは異なります。

なぜ複数のサービスが共存できるか

なぜモバイル Suica と WAON が入ったおサイフケータイが改札機を通るとき、ちゃんとモバイル Suica が反応してくれるのでしょうか。

FeliCa ではカードを読み取ろうとするときに Polling という処理を行います。

リーダ/ライタがカードを捕捉するには、Polling コマンドによるポーリングとよばれる不特定多数のカードへのよびかけを行います。捕捉したいカード(システム)の指定には、「3.2.1 システム定義情報」で説明したシステムコードを使用します。Polling コマンドによるポーリングを行うと、カードはその応答として IDm および PMm を返します。取得した IDm を使用することによって、以降、特定のカードとのみ通信できるようになります。

※FeliCa カード ユーザーズマニュアル 抜粋版 Ver 2.01 より

つまり、改札機を通るとき、改札機のICカード読み取り部からは「0x0003 のシステムがほしいでーす」というコマンドが出ており、0xFE00 を持つ WAON ではなく 0x0003 を持つ Suica が反応する、ということになります。

なお、はじめに例で挙げた 楽天Edy、nanaco、WAON の3つは 0xFE00 の FeliCa システムコードを用いています。
例えば WAON を読み取りたいときは「0xFE00 のシステムがほしいでーす」というコマンドを出せばいいのですが、これでは同じ FeliCa システムコードをもつ 楽天Edy、nanaco も補足できることになります。
複数の電子マネーサービスに対応している読み取り端末で、あらかじめボタンを押すなどして利用する電子マネーを選択するのは、使いたい電子マネーサービスを正しく読み取りにいけるようにするためです。

現に、この電子マネー選択機能を持っていない iOS App、Japan NFC Reader にモバイル Suica と 楽天Edy 等が入っているおサイフケータイを近づけると、Suica の情報しか読み取ってくれません(執筆時点での最新バージョン Ver 0.2.9 において)。交通系ICを優先して読み取るようにプログラムしているためです。

本題

さて、FeliCa カードを読み取るときには、読み取りたい FeliCa システムコードを用いて Polling コマンドを使う、ということはわかりました。では、複数の FeliCa システムを横断してカードを読み取りたいときはどうすればいいでしょうか。
「FeliCa カード ユーザーズマニュアル 抜粋版」の「3.2.4 システム切り替え」にはこのようにあります。

システム切り替えを行うには、以下の 2 つの方法があります。
- Polling コマンドによるシステム切り替え
- IDm の指定によるシステム切り替え

Polling コマンドによってシステムを切り替えてから各コマンドを利用するか、各コマンドを利用するときにその FeliCa システムコードが持つ IDm を指定することでシステムの切り替えを行うか、の2つの方法が記されています。

しかし、iOS 13 の Core NFC では「IDm の指定によるシステム切り替え」ができません。

FeliCa 用のコマンドに IDm を指定する術がない

FeliCa を読み取るためによく使われるいくつかのコマンドは protocol NFCFeliCaTag によって定義されています。例えば、Read Without Encryption コマンドは

@available(iOS 13.0, *)
func readWithoutEncryption(serviceCodeList: [Data], blockList: [Data], completionHandler: @escaping (Int, Int, [Data], Error?) -> Void)

となっており、IDm を指定する引数がありません。また、protocol NFCFeliCaTag には currentIDm が用意されていますが、これも

@available(iOS 13.0, *)
var currentIDm: Data { get }

となっており、set することができません。

polling(systemCode:requestCode:timeSlot:completionHandler:)

以上の理由から、iOS 13 の Core NFC では「Polling コマンドによるシステム切り替え」のみが有効な方法となります。その際に使用する関数は

@available(iOS 13.0, *)
func polling(systemCode: Data, requestCode: PollingRequestCode, timeSlot: PollingTimeSlot, completionHandler: @escaping (Data, Data, Error?) -> Void)

と定義されています。

本来であれば FeliCa を読み取る際にこの Polling コマンドを必ず使用するのですが、Core NFC で NFCTagReaderSession.begin() を行い、tagReaderSession(_:didDetect:) にタグの情報がやってくるときにはすでに Polling が終わっているので、このコマンドは登場していませんでした。

ここで引数に指定できる systemCode

System code must be one of the provided values in the "com.apple.developer.nfc.readersession.felica.systemcodes" in the Info.plist; NFCReaderErrorSecurityViolation will be returned when an invalid system code is used. Polling with wildcard value in the upper or lower byte is not supported.

となっています。

Info.plist であらかじめ使う FeliCa システムコードを指定しなければならない

これまでの Core NFC Advent Calendar 2019 の記事で、FeliCa カードを読み取るサンプルプロジェクトを紹介しましたが、そのどれでも「Info.plist の設定」というセクションがありました。

スクリーンショット 2019-12-15 0.03.43.png

あらかじめ、アプリで使用する FeliCa システムコードを Info.plist に加えておかなければならないのです。そして、ここではワイルドカード(0xFF)が使用できないことになっています。また、Info.plist に記述するため、動的に変更することもできません。

実例

私が開発している Japan NFC Reader の Info.plist を改変しつつ、デモを行います。

私が持っている学生証は、大学生協ICプリペイドの仕様に準拠していて、また学生証内にある FeliCa システムコードは

  • 0x8E4B
  • 0xFE00

の2つです。これを以下のように 0xFE00 のみを Info.plist で指定して読み取ると
スクリーンショット 2019-12-17 0.30.13.png
このような結果となり、Info.plist で指定していない 0x8E4B には接続できず、IDm を取得できていません。

しかし、両者を Info.plist で指定して読み取ると
スクリーンショット 2019-12-17 0.30.27.png
0x8E4B0xFE00 の両方の IDm を正しく取得することができました。

使用したコード

前々回前回紹介したサンプルコードのうち、tagReaderSession(_:didDetect:) を今回の内容に合わせて改変したものがこちらになります。

1つ目の print では 0x8E4B の IDm が出力され、その後 Polling コマンドで 0xFE00 を指定し、2つ目の print でその IDm が出力されます。

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    print("tagReaderSession(_:didDetect:)")

    let tag = tags.first!
    session.connect(to: tag) { (error) in
        if let error = error {
            session.invalidate(errorMessage: error.localizedDescription)
            return
        }

        guard case NFCTag.feliCa(let feliCaTag) = tag else {
            session.invalidate(errorMessage: "FeliCa ではない")
            return
        }

        session.alertMessage = "カードを読み取っています…"

        var currentSystemCode = feliCaTag.currentSystemCode.map { String(format: "%.2hhx", $0) }.joined()
        var currentIDm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined()
        print("currentSystemCode:", currentSystemCode, "currentIDm:", currentIDm) // currentSystemCode: 8e4b currentIDm: 012e457707ce323a

        let systemCode = Data([0xFE, 0x00])
        feliCaTag.polling(systemCode: systemCode, requestCode: .systemCode, timeSlot: .max1) { (pmm, systemCode, error) in

            currentSystemCode = feliCaTag.currentSystemCode.map { String(format: "%.2hhx", $0) }.joined()
            currentIDm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined()
            print("currentSystemCode:", currentSystemCode, "currentIDm:", currentIDm) // currentSystemCode: fe00 currentIDm: 112e457707ce323a

            session.alertMessage = "完了"
            session.invalidate()
        }
    }
}

スクリーンショット 2019-12-17 0.46.14.png

後記

今回の記事は結構駄文でした。これに関連した他の記事もこれからの Advent Calendar で公開予定ですので、その執筆に合わせて再推敲、多少の内容変更をするかもしれません。

また、この iOS 13 Core NFC で IDm の指定によるシステムの切り替えができない現状をバグであるとして GitHub の IssueApple Developer Forums に挙げている方もいらっしゃいます。私はこの Core NFC で初めて FeliCa を用いたアプリケーションを開発しましたので、そもそも IDm でシステムを切り替えられることを知りませんでした。もし、Android や Windows で FeliCa を扱っていて、より詳しい情報をお持ちの方はこれらの議論に参加してみてはいかがでしょうか。

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