やもりの技術ブログ

日々の生活で行った技術的な活動や日頃感じたことを書きます。

【開発】Suica(PASMO)の利用履歴を読み出す話

こんにちは、やもり(yamori-tech)です。

開発として「SuicaPASMO)の利用履歴を読み出す話」について書こうと思います。学生証や免許書なども、大体同じような流れで読み出せます。

結論としては、Mac から パソリ rc-s380(Felicaリーダー)、 nfcpy を install 利用し、モバイルSuicaのデータを読み出せました。また、同じソースコードで、スマートICOCAやPASMOの情報も読み出せました。


www.youtube.com

 

なぜ作ったか

今頃感が満載ですが、以前からNFCの仕組みや読み取り方法などについて、学びたい、身近にあるSuicaPASMO情報を見てみたい、と思っていたので、やってみました。
昨年、特別定額給付金の受給申請のため、パソリを買ったのも大きなきっかけです。

どのように作ったか

方針

NFC向けライブラリとして、pythonnfcpyがあり、比較的に自由に扱えるようなので、今回はこちらを利用します。基本的な使い方はnfcpyの公式ページに書いてあります。

nfcpy.readthedocs.io

Suicaの利用履歴を読み出す方法やNFCの仕組みについては多くの先人が記録を残してくれています。下記サイトが非常に参考になりました(一部ソースを使わせていただきました)。ありがとうございます!

SuicaPASMO)の利用履歴を読み出す概要

NFCの特徴は「かざす」ことでデータを通信できることなので、モバイルSuicaをリーダーに近づけると利用履歴が出力される仕組みにしたいと思います。利用履歴はMAXで20セットあるようです。

全体概要は以下の図になります。利用履歴の読み出しは、20セットを1つずつ、何かしらのキーを押すたびに読み出すようにしました。

 

f:id:yamori-tech:20210516004023p:plain

全体概要(overview)

構築環境

setup

    1. lsusbのインストール

      nfcpyを利用するまえに、パソリがMacに認識されているかを確認します。

      lsusbコマンドを利用することで、usb経由で認識されるデバイスを確認できます。

      % brew install lsusb
      % lsusb
      -> Bus 020 Device 001: ID 054c:06c1 Sony Corporation RC-S380/S Serial: 1142869

      が、、実はlsusbを使わなくても確認できます。 

      system_profiler SPUSBDataType をterminal から実行すると同じ内容が見れます。

      mac にlsusb をインストール  ↑気になる方は、こちらサイトでご確認ください。

    2. nfcpyのインストール&サイバネコードのダウンロード
      公式に従いインストールします。

      % pip install nfcpy

      サイバネコードが記載されたcsvファイルをダウンロードしておきます。(m2wasabi/nfcpy-suica-sample で公開されています、今回はこちらからcsvを頂いています。)

      サイバネコードとは「鉄道の自動改札機において、複数の鉄道事業者を経由する連絡運輸の場合に、鉄道事業者をまたいで発着駅や経由駅を認識できる必要性から定められたコード」のことを指します。from 駅コード - Wikipedia

      Suicaに記録されるデータ(乗車駅、降車駅)を解析する際に必要です。

      サイバネコードの詳細については、下記サイトが参考になります。

      自動改札機の研究
      サイバネ駅コードの分析というかまとめ

実装内容

 python での実装内容は以下になります。

#!/usr/bin/env python
# coding: utf-8
import binascii
import nfc
import ndef
import os
import struct
import pandas as pd
from IPython.display import display

## iccoca service_code
service_code = 0x090f
## entry name
entry_name = {0:"端末種", 1:"処理", 2:"??", 3:"??",
                        4:"日付", 5:"日付(??)", 6 : "入線区", 7:"入駅順",
                        8:"出線区", 9:"出駅順", 10:"残高 (little endian)", 11:"残高 (little endian)",
                        12:"連番", 13:"連番", 14:"連番", 15:"リージョン"}

# nfc用
# Suica待ち受けの1サイクル秒
TIME_cycle = 1.0
# Suica待ち受けの反応インターバル秒
TIME_interval = 0.2
# タッチされてから次の待ち受けを開始するまで無効化する秒
TIME_wait = 3
# NFC接続リクエストのための準備
# 212F(FeliCa)で設定
target_req_suica = nfc.clf.RemoteTarget("212F")
# 106B(免許書ナド)で設定
#target_req_suica = nfc.clf.RemoteTarget("106B")
# 0003(Suica)
target_req_suica.sensf_req = bytearray.fromhex("0000030000")

# 読み出し回数
read_num = 20

class MyCardReader(object):
    def read_csv(self, filename, names):
      #print('-- read file-- :', filename)
      return pd.read_csv(filename, names=names)

    def get_station(self, line_key, station_key):
      # 線区コードと駅コードに対応するStationRecordを検索する
      df = self.read_csv('StationCode.csv', names=('kind', 'line_key', 'station_key', 'company_name', 'line_name',  'station_name'))
      line = df.query('line_key==@line_key and station_key==@station_key').line_name
      station = df.query('line_key==@line_key and station_key==@station_key').station_name
      return (line, station)
      #return ["0", "0", "0", "None", "None", "None"]

    def get_year(self, date):
      return (date >> 9) & 0x7f

    def get_month(self, date):
      return (date >> 5) & 0x0f

    def get_day(self, date):
      return (date >> 0) & 0x1f

    def get_console(self, key):
      # よく使われそうなもののみ対応
      return {
        0x03: "精算機",
        0x04: "携帯型端末",
        0x05: "車載端末",
        0x07: "券売機",
        0x08: "券売機",
        0x09: "入金機",
        0x12: "券売機",
        0x16: "改札機",
        0x1c: "乗継精算機",
        0xc8: "自販機",
        }.get(key)

    def get_process(self, key):
      # よく使われそうなもののみ対応
      return {
        0x01: "運賃支払",
        0x02: "チャージ",
        0x0f: "バス",
        0x46: "物販",
        }.get(key)

## カードデータにアクセス、解析、結果表示 def read_recent_log(self, tag): if isinstance(tag, nfc.tag.tt3.Type3Tag): print('\(^o^)/') try: for i in range(read_num): sc = nfc.tag.tt3.ServiceCode(service_code >> 6, service_code & 0x3f) bc = nfc.tag.tt3.BlockCode(i, service=0) data = tag.read_without_encryption([sc], [bc]) # ビッグエンディアンでバイト列を解釈する byte_data = bytes(data) row_be = struct.unpack('>2B2H4BH4B', byte_data) # リトルエンディアンでバイト列を解釈する row_le = struct.unpack('<2B2H4BH4B', data) tmp_data = ['%02x ' %s for s in data] charge_flag = 0 print("== {} ==".format(i)) #print("tmp_data: "+" ".join(tmp_data)) #print("row_be: ", row_be) #print("row_le: ", row_le) ## 各処理について for m, n in enumerate(data): if m == 0: print(entry_name[m], self.get_console(n)) elif m == 1: print(entry_name[m], self.get_process(n)) if self.get_process(n) in ["チャージ", "物販"]: charge_flag = 1 elif m == 4: print("year-month-day: {}-{}-{}".format(self.get_year(row_be[3]), self.get_month(row_be[3]), self.get_day(row_be[3]))) elif m == 5: continue elif m in [6, 7]: if charge_flag == 1 or m == 7: continue station_info = self.get_station(row_be[4], row_be[5]) print('乗車 line&station: ', station_info[0].values, station_info[1].values) elif m in [8, 9]: if charge_flag == 1 or m == 9: continue station_info = self.get_station(row_be[6], row_be[7]) print('降車 line&station: ', station_info[0].values, station_info[1].values) elif m in [10, 11]: if m == 11: continue print(entry_name[m], row_le[8], '円') else: tmp_val = '%02x ' %n print(entry_name[m], int(tmp_val, base=16)) input() print('\n') except Exception as e: print('error: %s' %e) else: print("error: tag isn't Type3Tag")
## tag情報を確認、ID取得 def on_connect(self, tag): #タッチ時の処理 print("【 Touched 】") #タグ情報を全て表示 #print(tag) #IDmのみ取得して表示 self.idm = binascii.hexlify(tag._nfcid) print("IDm : " + str(self.idm)) #特定のIDmだった場合のアクション if self.idm == "00000000000000": print("【 登録されたIDです 】") #read_recent_logしてみる print("\nread_recent_log:") self.read_recent_log(tag) return True def read_id(self): ## status = False
## NFCリーダーを認識 clf = nfc.ContactlessFrontend('usb') while True:
## Tagの種類を指定、検索 target_res = clf.sense(target_req_suica, iterations=int(TIME_cycle//TIME_interval)+1 , interval=TIME_interval) ## 情報を取得できたら if isinstance(target_res, nfc.clf.RemoteTarget): print(target_res, type(target_res)) tag = nfc.tag.activate(clf,target_res) tag.sys = 3 status = self.on_connect(tag) if status == True: clf.close() break else: continue return if __name__ == '__main__': cr = MyCardReader() while True: #最初に表示 print("Please Touch") #タッチ待ち cr.read_id() #リリース時の処理 print("【 Released 】") break

動作確認の内容は、最初の動画を見ていただければと思います。

まとめ

敷居が高そう、、と思っていたのが、意外と簡単に読み出すことができました。
個人IDに紐付くもの(よくあるものは、ロック解除や退勤システム、などでしょうか)とも相性が良さそうですね。

今後は「かざす」動作と組み合わせてできる便利な(楽しい)仕組みを思いついたら、実装してみようと思います。 

【余談】そもそも、NFCってなんぞや?

NFCと聞くと、キャッシュレス決済の一つというイメージの方も多いのではないでしょうか?www.smbc-card.com

↑ キャッシュレス決済の基礎知識 by 三井住友カードの記事によると、キャッシュレス決済とは、「クレジットカードや電子マネー、口座振替を利用して、紙幣・貨幣を利用せずに支払いや受け取りを行う決済方式のこと」であり、大きく3つの区分けとサービス例が存在します。

  • プリペイド(前払い)ーー>  例)電子マネー(交通系、流通系)
  • 即時払い(リアルタイムペイ)ーー>  例)デビットカード(銀行系など)
  • 後払い(ポストペイ)ーー>  例)クレジットカード(磁気カード、IC)

※ 近年流行りの QRコード決済、バーコード決済は、上記いづれも対応可能

 

NFC(Near Field Communication:近距離無線通信)は、上記のプリペイド決済、具体的には、例えば電子マネーのSuicaやPASMOで利用される通信技術になります。

より簡単に理解すると、NFCとは「端末をかざすだけで通信ができる技術」のこと※1、であり、FelicaはNFCの規格の一つです※2。「端末」はカード、スマホ、リーダー、シール、など多岐に渡ります。

 

NFCは「かざす」動作にひも付き情報をやり取りできるため、わかりやすいシステムの実現につながる重要な技術として、近年注目を集めています。すでに実社会でも普及している身近な技術ですね。

 

【参考】

※1 PayPayの NFCとは?スマホに搭載された便利な機能について解説します 記事から引用させていただきました

※2 ソニー株式会社 | FeliCa | NFCについて | NFCの定義