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

iPhone で FeliCa を読み取るライブラリを作りました

この記事は potatotips #64 で発表した内容をテキスト化したものです。

TL;DR

サクッと FeliCa の IC カードを読み取れるライブラリを作ったよ。
今のところ下記のカードに対応してるよ。

  • Suica, Pasmo, Kitaca, ICOCA, TOICA、manaca、PiTaPa、nimoca、SUGOCA、はやかけん
  • nanaco、Edy、WAON
  • カスタムタグ

https://github.com/tattn/NFCReader

iOS 13 から FeliCa の読み書きができるように

WWDC 19 で CoreNFC で FeliCa が読み書きできるようになったことが発表されました。
FeliCa は Suica、Pasmo などの交通系 IC や nanaco、WAON などの電子マネーが採用している非接触 IC カードの技術方式です。

https://developer.apple.com/videos/play/wwdc2019/715/

Suica を例に読み取り方を紹介

例として Suica の乗降履歴の読み取り手順を紹介します。

CoreNFC で FeliCa を読み込むときのフロー

image.png

https://www.sony.co.jp/Products/felica/business/tech-support/index.html
https://developer.apple.com/documentation/corenfc

プロジェクト設定

Info.plist に Privacy 設定と System Code の追加が必要です。

image.png

System Code はシステムごとに割り当てられた 2 バイトのコードです。 plist に追加していないカードは読み取ることができません。
Suica の場合は 0003 になります。

また、Capabilities に 
Near Field Communication Tag Reading を追加する必要があります。

Session の作成 & ポーリングの開始

let session = NFCTagReaderSession(
    pollingOption: NFCTagReaderSession.PollingOption.iso18092, 
    delegate: self
)
session.alertMessage = "iPhoneをSuicaに近づけてください"
session.begin()

FeliCa を読み込む時は ISO18092 を指定します。これは PollingOption のドキュメントコメントにも記載されています。

alertMessage を指定すると、読み込みが始まったタイミングで表示される View にそのテキストが表示されます。
begin を呼び出すと、タグの読み取り (ポーリング) が開始されます。

タグとの接続

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

    guard case .feliCa(let tag) = tags.first else { return }

    session.connect(to: tag) { error in
        guard error == nil else { return }

        // Tagの読み込み (次のセクションに続く)
    }
}

タグが検出されると、delegate (NFCTagReaderSessionDelegate) が呼ばれます。
タグは同時に複数検出される場合がありますが、今回はその時のハンドリングは省略します。

NFCFeliCaTag が取得できたら、connect メソッドを呼び出して、タグに対してコマンドを送れるようにします。

Suica のブロックデータの読み込み

let serviceCodeList = [Data([0x0f, 0x09])]               // サービス(データ)を特定するコード
let blockList = (0..<UInt8(10)).map { Data([0x80, $0]) } // データの取得方法/位置を決める

tag.requestService(nodeCodeList: serviceCodeList) { nodes, error in
    guard error == nil, nodes.first != Data([0xff, 0xff]) else {
        return
    }

    tag.readWithoutEncryption(
        serviceCodeList: serviceCodeList,
        blockList: blockList) { status1, status2, dataList, error in
            guard error == nil, status1 == 0, status2 == 0 else {
                return
            }

            // dataの読み込み (次のセクションに続く)
    }
}

まずは、requestService を呼び出して、そのタグに読み取ろうとしているデータ (サービス) があるかどうかを確認します。
サービスが存在しない場合は 0xFF, 0xFF が返ります。 (FeliCa の仕様に基づく)

サービスが存在した場合は、readWithoutEncryption を呼び出して、データの取得をします。 (今回読み取るデータが認証不要なサービスのため、readWithoutEncryption になります)

FeliCa の仕様で、ステータスフラグがともに 0 のときのみ、正しいデータが取得可能なため、念の為チェックしておくと良さそうです。

Suica データのデコード

for data in dataList {
    let year = data[4] >> 1
    let month = UInt16(bytes: data[4...5]) >> 5 & 0b1111
    let day = data[5] & 0b11111
    print("利用日: \(year)/\(month)/\(day)") // 19/8/27

    let entrance = UInt16(bytes: data[6...7])
    let exit = UInt16(bytes: data[8...9])
    print("入場駅: \(entrance), 出場駅: \(exit)")

    let balance = UInt16(bytes: data[10...11].reversed())
    print("残高: ", balance)
}

下記のページを参考にバイナリデータから必要なデータを読み取ります。
https://www.wdic.org/w/RAIL/サイバネ規格%20(ICカード)

(year が 2000 年基準なのが面白いです)

Swift でバイナリデータを数値型に変換するのは少し手間なので上記では以下のようなエクステンションを利用しています。

extension FixedWidthInteger {
    init(bytes: UInt8...) {
        self.init(bytes: bytes)
    }

    init<T: DataProtocol>(bytes: T) {
        let count = bytes.count - 1
        self = bytes.enumerated().reduce(into: 0) { (result, item) in
            result += Self(item.element) << (8 * (count - item.offset))
        }
    }
}

↓のような感じで使えるので便利です。

XCTAssertEqual(UInt16(bytes: 0x35, 0x0B), 13579)
XCTAssertEqual(Int(bytes: 0x07, 0x5B, 0xCD, 0x15), 123456789)

便利なライブラリを作りました

前述のように読み取りには結構実装が必要で、面倒です。(エラーハンドリングなどを含めるとより手間)
そこで、サクッと使えるライブラリにしてみました。

https://github.com/tattn/NFCReader

let reader = Reader<Suica>()

reader.read(didBecomeActive: { _ in
    print("読み込み開始")
}, didDetect: { reader, result in
    switch result {
    case .success(let suica):
        let balance = suica.boardingHistories.first?.balance ?? 0
        reader.setMessage(balance)
    case .failure(let error):
        reader.setMessage("読み込みに失敗しました")
    }
})

このような感じで Reader の型パラメータに読み取りたい NFC タグを指定するだけで struct にマッピングされたデータを取得できます。

Suica の他にも以下のようなタグを読み取れます。

image.png

ぜひ使ってみてください。
NFC タグの追加プルリクエストなども大募集中です。

まとめ・所感

CoreNFC を使って FeliCa (Suica) を読み込む方法と作ったライブラリを紹介しました。

Suica の残高領域は 2 bytes しか用意されてないので
入金できる上限を簡単には増やせなさそうだなという発見もありました。

FeliCa の仕様はソニーが日本語で
丁寧に書いているのでとてもわかりやすかったです。
一方 Apple のドキュメントには現時点では全然情報がなく、FeliCa の仕様を知っている人でないと読めない感じでした。

参考文献

tattn
Yahoo! JAPANで乗換案内アプリの開発や社内のアプリの課題解決、新規技術の導入サポートなどをしています。 https://github.com/tattn https://twitter.com/tanakasan2525
https://twitter.com/tanakasan2525
yahoo-japan-corp
Yahoo! JAPAN を運営しています。
https://www.yahoo.co.jp
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
ユーザーは見つかりませんでした