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

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

- [iPhone で電子マネー（楽天Edy、nanaco、WAON）を読み取ってみよう！](https://qiita.com/treastrain/items/f5c518486d19ba3573d3)
- [iPhone で交通系IC（Suica、PASMO、ICOCA、…etc.）を読み取ってみよう！](https://qiita.com/treastrain/items/d54ef2c2abbcd40fb9f1)

そして 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](https://japannfcreader.tret.jp) にモバイル 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 コマンドは

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

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

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

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

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

```swift
@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 の設定」というセクションがありました。

<img width="909" alt="スクリーンショット 2019-12-15 0.03.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/f6be8d55-c108-ab50-627b-57355e3a2632.png">

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

# 実例
私が開発している [Japan NFC Reader](https://japannfcreader.tret.jp) の Info.plist を改変しつつ、デモを行います。

私が持っている学生証は、[大学生協ICプリペイドの仕様](https://github.com/treastrain/TRETJapanNFCReader#大学生協icプリペイド-tretjapannfcreaderfelicaunivcoopicprepaid)に準拠していて、また学生証内にある FeliCa システムコードは

- `0x8E4B`
- `0xFE00`

の2つです。これを以下のように `0xFE00` のみを Info.plist で指定して読み取ると
<img width="449" alt="スクリーンショット 2019-12-17 0.30.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/46d9f6f2-bf9a-8480-7d78-7e4ae0d1bc25.png"><img width="449" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/6d03a4bd-b4f8-ebd6-ae4d-056137fb1507.jpeg">
このような結果となり、Info.plist で指定していない `0x8E4B` には接続できず、IDm を取得できていません。

しかし、両者を Info.plist で指定して読み取ると
<img width="449" alt="スクリーンショット 2019-12-17 0.30.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/f2705996-2f49-f4bd-4306-ef845638e49c.png"><img width="449" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/4268a605-c5ab-a58f-b708-a06bba4ab432.jpeg">
`0x8E4B` と `0xFE00` の両方の IDm を正しく取得することができました。


# 使用したコード
[前々回](https://qiita.com/treastrain/items/f5c518486d19ba3573d3)や[前回](https://qiita.com/treastrain/items/d54ef2c2abbcd40fb9f1)紹介したサンプルコードのうち、`tagReaderSession(_:didDetect:)` を今回の内容に合わせて改変したものがこちらになります。

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

```swift
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()
        }
    }
}
```

<img width="403" alt="スクリーンショット 2019-12-17 0.46.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/a1073a20-11f0-d4bf-394e-a5b223a8ceb7.png">

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

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