4. カードリーダーと連携する – SuicaのIDを読み取る
この記事はiOSDC2022で発表したセッション、Swiftで我が家を より便利に、安全に!の発表の第4部の内容をまとめたものになります。
この記事は、「3. ラズパイでセンサーの値を読み取る – 重量センサーの値を取得する」 の記事の続編です。もしまだ読んでいない方は読んでいただけると!
この記事では、Pasoriリーダーでのカードの読み取りと、SwiftからPythonコードの実行方法を紹介します。
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. 人感センサーを使う – こちらから攻める」です!
参考
- ラズパイにパソリを繋いでNFCタッチ!
- NFC(免許証やおサイフケータイ)のIDmやUIDがリーダーで読み取るたびに変わる問題 – PaSoRiでの読み取り実装
- From Swift Import Python