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

iPhone で交通系IC(Suica、PASMO、ICOCA、…etc.)を読み取ってみよう!

運転免許証物販向け電子マネー(楽天Edy、nanaco、WAON)に引き続き、今回は交通系ICである Suica、PASMO、ICOCA などの残高を iPhone で読み取ってみましょう!

環境

  • 開発
    • Xcode Version 11.3 (11C29)
    • Apple Swift version 5.1.3 (swiftlang-1100.0.282.1 clang-1100.0.33.15).
    • macOS Catalina 10.15.2(19C57)
  • 実機
    • iPhone 11 Pro (A2215、MWCC2J/A)
    • iOS 13.3 (17C54)

実行結果

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

Xcode プロジェクトの作成

Create a new Xcode project から iOS の Single View App を作成します。

Capability と Entitlements の設定

Project の Target から Signing & Capabilities を選択し、「+ Capability」から「Near Field Communication Tag Reading」を追加します。
スクリーンショット 2019-12-11 0.41.35.png
すると、「<Product-Name>.entitlements」というファイルが追加されているので、「Near Field Communication Tag Reader Session Formats」に「NFC Data Exchange Format」および「NFC tag-specific data protocol」があることを確認します。

Info.plist の設定

Info.plist に「Privacy - NFC Scan Usage Description」と「ISO18092 system codes for NFC Tag Reader Session」を追加します。
「Privacy - NFC Scan Usage Description」には NFC を何のために使用するのかについての説明を、「ISO18092 system codes for NFC Tag Reader Session」の配下には以下の FeliCa システムコードを記述します。

  • 0003

スクリーンショット 2019-12-15 0.03.43.png
以上で設定はおしまいです。

ファイルツリー

今回のプロジェクトのファイルツリーを確認しておきます。
スクリーンショット 2019-12-11 0.48.34.png
前回の記事と全く同じです。処理は全て ViewController.swift に記述します。

コーディング

ライブラリを導入するか、導入せずに直接 Core NFC を操るかの2択になります。。

ライブラリを導入する場合

前回の記事でも登場した、私が開発・公開しているライブラリ、treastrain/TRETJapanNFCReader は交通系ICの残高の取得にも使用できます。導入方法は README をご覧いただくとして、導入後のサンプルコードを記載します。

treastrain/TRETJapanNFCReaderを使った場合
import UIKit
import TRETJapanNFCReader
class ViewController: UIViewController, FeliCaReaderSessionDelegate {

    var reader: TransitICReader!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.reader = TransitICReader(viewController: self)
        self.reader.get(itemTypes: [.balance])
    }

    func feliCaReaderSession(didRead feliCaCard: FeliCaCard) {
        let transitICCard = feliCaCard as! TransitICCard
        let balance = transitICCard.data.balance! // カード残高
    }
}

直接 Core NFC を使う場合

こちらも前回の記事とコードの内容はほぼ同一ですので、コードの各部分の説明は前回の記事を参考にしてください。異なる部分のみ解説します。

ViewController.swift
import UIKit
import CoreNFC

class ViewController: UIViewController, NFCTagReaderSessionDelegate {

    var session: NFCTagReaderSession?

    override func viewDidLoad() {
        super.viewDidLoad()

        guard NFCTagReaderSession.readingAvailable else {
            print("NFC タグの読み取りに非対応のデバイス")
            return
        }

        self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
        self.session?.alertMessage = "カードの上に iPhone の上部を載せてください"
        self.session?.begin()
    }

    func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
        print("tagReaderSessionDidBecomeActive(_:)")
    }

    func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
        let readerError = error as! NFCReaderError
        print(readerError.code, readerError.localizedDescription)
    }

    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 = "カードを読み取っています…"

            let idm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined()
            print("IDm:", idm)

            /// FeliCa サービスコード
            let serviceCode = Data([0x00, 0x8B].reversed())

            /// ブロック数
            let blocks = 1
            let blockList = (0..<blocks).map { (block) -> Data in
                Data([0x80, UInt8(block)])
            }

            feliCaTag.readWithoutEncryption(serviceCodeList: [serviceCode], blockList: blockList) { (status1, status2, blockData, error) in
                if let error = error {
                    session.invalidate(errorMessage: error.localizedDescription)
                    return
                }

                guard status1 == 0x00, status2 == 0x00 else {
                    print("ステータスフラグがエラーを示しています", status1, status2)
                    session.invalidate(errorMessage: "ステータスフラグがエラーを示しています s1:\(status1), s2:\(status2)")
                    return
                }

                let data = blockData.first!
                let balance = data.toIntReversed(11, 12)

                print(data as NSData)
                print("残高: ¥\(balance)")
                session.alertMessage = "残高: ¥\(balance)"
                session.invalidate()
            }
        }
    }
}

extension Data {
    /// https://github.com/treastrain/TRETJapanNFCReader/blob/master/TRETJapanNFCReader/Extensions.swift#L112
    func toIntReversed(_ startIndex: Int, _ endIndex: Int) -> Int {
        var s = 0
        for (n, i) in (startIndex...endIndex).enumerated() {
            s += Int(self[i]) << (n * 8)
        }
        return s
    }
}

コード解説

前回の記事を参照

対応端末かどうかを確かめる
NFCTagReaderSession をスタートさせる
NFCTagReaderSessionDelegate
タグに接続する
Read Without Encryption コマンドを送信

FeliCa サービスコードの指定

/// FeliCa サービスコード
let serviceCode = Data([0x00, 0x8B].reversed())

/// ブロック数
let blocks = 1
let blockList = (0..<blocks).map { (block) -> Data in
    Data([0x80, UInt8(block)])
}

交通系ICカードの残高情報は FeliCa サービスコード 0x008B に1ブロックで記録されています。リトルエンディアンで送信するために .reversed() しています。

データの解析

let data = blockData.first!
let balance = data.toIntReversed(11, 12)

print(data as NSData)
print("残高: ¥\(balance)")
session.alertMessage = "残高: ¥\(balance)"
session.invalidate()

Read Without Encryption コマンドの結果として返ってきた blockData の要素数は1です。
その唯一の要素である data の11番目と12番目の2ビットにリトルエンディアンで残高が記録されています。
NFC の通信処理をすべて終えたら session.invalidate() で終了します。

後記

内容としては楽天Edy、nanaco、WAON の読み取りとほぼ同一で、指定する FeliCa システムコード、FeliCa サービスコードが異なるというものでした。
都市部であればどうしても交通系ICが生活の一部になっているかと思いますので、あえて別記事として執筆しました。
自分のいつも使う交通系ICと iPhone でちょっと遊んでみませんか…?

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