Event

4. カードリーダーと連携する – SuicaのIDを読み取る

4. カードリーダーと連携する – SuicaのIDを読み取る

この記事はiOSDC2022で発表したセッション、Swiftで我が家を より便利に、安全に!の発表の第4部の内容をまとめたものになります。
この記事は、「3. ラズパイでセンサーの値を読み取る – 重量センサーの値を取得する」 の記事の続編です。もしまだ読んでいない方は読んでいただけると!
この記事では、Pasoriリーダーでのカードの読み取りと、SwiftからPythonコードの実行方法を紹介します。

3. ラズパイでセンサーの値を読み取る - 重量センサーの値を取得するこの記事は、「Swiftで我が家をより便利に、安全に」の発表の3部「3. ラズパイでセンサーの値を読み取る - 重量センサーの値を取得する 」の内容をまとめた記事になります!...

Pasoriで、SuicaのIDを読み取る

この記事では、ラズパイとPasori(RC-S380↓)を使って、SuicaのIDを読み取る実装方法を紹介します。
Suicaなどのカードには事前にIDが振られているので、そのIDが事前に登録したIDと一致するかを確認します。
一致している場合は、在宅していることとし、その判定結果は次の部で使用します。

ちなみに今回は家に余っていたSuicaを使用しますが、当初はMobile Suicaを使用する想定でした。
しかしFelicaの仕様によりMobileSuicaは読み込むたびにIDが変化するようだったので、今回はハードのSuicaで検証します(実際にIDを読み取ってみたら、毎回値が違ったので、おかしいと思った)
参考: モバイル FeliCa IC チップにおける製造 ID(IDm)の取り扱いについて

今回はこのPasoriを玄関にあるラックにこっそり置いて、帰宅時にSuicaをかざして認証することを想定しています。

Pasori連携の実装詳細

実装のイメージは以下の図の通りです
事前にSwiftHomeAppでSuicaのIDを読み取り、SwiftHomeServer経由で、ラズパイに送信し、保存します。
そしてPasoriにSuicaをかざし、IDが一致するか認証します。
今回の実装ではSuicaのみを対象にしていますが、設定次第で他のカードも利用できるはずです

今回Pasoriでデータを読み取るために、nfcpyというPythonのライブラリを使用します
ちょっとSwiftで全ての実装は難しそうだったので、データの読み取り処理はPythonで記述し、それをSwiftから呼び出すという構成にしました。

Pythonを使って、Pasoriのデータを読み取る

まずはラズパイにnfcpyをインストールします
pipを使って、nfcpyをインストールします。

そしてラズパイのUSBポートにPasoriを繋ぎ、ラズパイ側でPasoriが認識されているかを確認します
②のコマンドを実行することで、USBデバイスの中で、Sonyを含む=Pasoriが接続できていることが確認できます。
ちなみにこのコマンドで認識されなかったのですが、③のコマンド実行→rebootをすることで、表示されるようになりました!(参考)

#① Install nfcpy
$ sudo pip install -U nfcpy
# ②In the list of USB devices, check if Pasori is recognized (ID below is masked)
$ lsusb | grep Sony
> Bus 001 Device 008: ID ******** Sony Corp. RC-S380/S
# ③if displayed as 'No such device'
$ python -m nfc
$ sudo reboot
# Ref: https://www.out48.com/archives/5396/

次に実際にnfcpyを使って、SuicaのIDを読み取るコードを書きます。
nfcpyの処理を使って取得したデータから、_nfcidという値を取り出し、それをdecodeして、その値を返却してます。

import nfc
import binascii
// Ref: https://qiita.com/h_tyokinuhata/items/2733d3c5bc126d5d4445
def read_id():
    print("Touch!")
    clf = nfc.ContactlessFrontend("usb")
    tag = clf.connect(rdwr={'on-connect': lambda tag: False})
    tag_id = binascii.hexlify(tag._nfcid).decode()
    clf.close()
    return tag_id

SwiftからPythonの処理を呼び出す

次にこのPythonの処理をSwift側から呼び出すコードです
今回は、Pythonを呼び出すコードを書くやすくするためのPythonKitというライブラリ、ファイルのパスを取得するためのPathKitというライブラリを使用するので、その設定をします

dependencies: [
     .package(url: "https://github.com/pvieito/PythonKit", branch: "master"),
     .package(url: "https://github.com/kylef/PathKit", from: "1.0.0"),
],

そしてメインのPythonコードを呼び出す処理を書きましょう
実装の仕方としては、①のようにまずは、呼び出したいPythonファイルがあるディレクトリを指定します。
今回はラズパイでの実行になるので、Path.currentはリポジトリをcloneしている階層になります。
そしてその後②のように、読み込みたいPythonのファイルをimportします
そして最後に③のように、Pythonのメソッドを指定することで、Pythonのコードを呼び出すことができます。

今回の場合は、このreadNfcIdメソッドを呼び出すと、実際にPasoriでSuicaをかざして、IDを読み取った後に、その文字列を返却するような挙動になります。

import PythonKit
import PathKit
func readNfcId() -> String {
    let sys = Python.import("sys")
    let path = "\(Path.current)/Sources/SwiftHomePi" // ①Specify the path of Python file
    sys.path.append(path)
    let readNfc = Python.import("read_nfc") ②import Python file
    return readNfc.read_id().description // ③Call the Read_id of the Python file
}

コード全文はこちらです!

ちなみにPythonのメソッドを呼び出すコードには、コンパイル時にないプロパティにアクセスできるDynamic Member Lookupという機能が使われています(参考)

最後に読み取ったIDと保存していたIDが一致するかどうかを確認する処理です

let id = PythonCall().readNfcId()
guard let savedNfcId = try await DataStore.shared.fetchNfcId() else { // ①Fetch registered ID
    print("Cannot find NfcId in sqlite DB")
    return
}
if id == savedNfcId.nfcId {
    print("This card is registered!")
} else {
    print("This card is not registered")
}

実装ができたら、動かしてみましょう!
今回は家にSuicaが2枚なかったので、Suicaとnanacoを使って検証しています!
無事Suicaだけ認証できていることが確認できました!🎉

iPhoneでSuicaを読み取る実装

ラズパイ側の実装は一通り終わったのですが、おまけとして、SwiftHomeAppに実装したSuicaを読み取るサンプルコードを記載します

そのまま実装する場合、CoreNFCというモジュールを利用するのですが、今回は実装しやすくするために、treastrain / Tanaka Ryogaさんが実装したTRETJapanNFCReaderというライブラリを使用しています。

コード自体は以下のように記載するだけで、SuicaのIDを取得できます

import Foundation
import CoreNFC
import TRETJapanNFCReader_FeliCa
import TRETJapanNFCReader_FeliCa_TransitIC

final class CardReaderViewModel: NSObject, ObservableObject {
    @Published private(set) var suicaId = ""
    private var reader: TransitICReader!
    private var transitICCardData: TransitICCardData?

    override init() {
        super.init()
        reader = TransitICReader(delegate: self)
    }

    func readCard() {
        reader.get(itemTypes: TransitICCardItemType.allCases)
    }
    // (Omit: Post Suica ID to SwiftHomeServer)
}

extension CardReaderViewModel: FeliCaReaderSessionDelegate {
    func feliCaReaderSession(didRead feliCaCardData: FeliCaCardData, pollingErrors: [FeliCaSystemCode : Error?]?, readErrors: [FeliCaSystemCode : [FeliCaServiceCode : Error]]?) {
        let suicaId = feliCaCardData.primaryIDm
        DispatchQueue.main.async {
            self.suicaId = suicaId
        }
    }

    func feliCaReaderSession(didInvalidateWithError pollingErrors: [FeliCaSystemCode : Error?]?, readErrors: [FeliCaSystemCode : [FeliCaServiceCode : Error]]?) {
    }

    func japanNFCReaderSession(didInvalidateWithError error: Error) {
    }
}

Suicaを読み取る場合、上記のコードに加えて、以下の設定が必要です

  • Info.plistにPrivacy - NFC Scan Usage Descriptionを追加する
  • Info.plistにISO18092 system codes for NFC Tag Reader Sessionを配列として追加し、そのアイテムに0003の値を追加します

この0003という値は、交通系ICカードを指定するコードになっていて、事前に読み取りたいものは全てここに登録しておく必要があります!
このコードについては、ライブラリのREADMEに詳細な記載があります。

これでreadCard()を呼び出すと、見覚えのあるカードリーダーの画面を表示できます!!🎉

まとめ

この記事では、Pythonで書いたPasoriのカード情報を読み取る処理をSwiftから呼び出す実装方法をまとめてみました!
Pasoriでの認証自体はかなり色々なことに応用できそうだなと思いつつ、Felicaなどの技術的な仕様をしっかり把握する必要があります。
この辺りの技術的なトピックはかなり面白そうなので、後ほど勉強してみたいと思いました!

次の記事は、「5. 人感センサーを使う – こちらから攻める」です!

5. 人感センサーを使う - こちらから攻めるこの記事は、「Swiftで我が家をより便利に、安全に」の発表の5部「5. 人感センサーを使う - こちらから攻める 」の内容をまとめた記事になります!...

参考

+1

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA