14
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

運転免許証物販向け電子マネー(楽天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 でちょっと遊んでみませんか…?

14
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?