以前、Android + RubotoにてFeliCaのKURURUの履歴を読んでみました。
Rubotoを使い、Androidで「長野市バス共通ICカード KURURU(くるる)」の履歴を読んでみた - メモ的な思考的な
そこで今回は Python2 + nfcpy でKURURUを読んでみます。
とはいえ、nfcpyでFeliCaを読む方法がよくわかりませんでした。
調べてみたところ、以下がとても参考になりました。ありがとうございます。
- m2wasabi/nfcpy-suica-sample: nfcpy 0.10.0 sample codes
- nfcpy で複数の System Code を持つ NFC タグを扱う方法 - uchan note
そこで、前者のリポジトリに含まれる suica_read.py
(Suicaを読むnfcpyコード) を理解しながら、KURURU用のコードを書いてみました。
なお、このレイヤを扱ったことがないため、考えたことを中心に書きます*1。もし誤りなどがありましたら、ご指摘ください。
目次
環境
suica_read.pyを理解する
suica_read.py
のソースコードを見たところ、いくつか分からないところがあったため、順に書いていきます。
なお、理解するために使った履歴データは、以前の履歴No.1のものを使いました。
struct.unpack('>2B2H4BH4B', data)
まずは
# ビッグエンディアンでバイト列を解釈する row_be = struct.unpack('>2B2H4BH4B', data) # リトルエンディアンでバイト列を解釈する row_le = struct.unpack('<2B2H4BH4B', data)
についてです。
参考にしたリポジトリでは、標準モジュールの struct.unpack()
モジュールを使って、Suicaデータを解析しています。
7.3. struct — 文字列データをパックされたバイナリデータとして解釈する — Python 2.7.14 ドキュメント
今回分からなかったのは、 >2B2H4BH4B
の部分です。
Pythonドキュメントを読むと、
>
は、ビッグエンディアンB
やH
は、何らかの数値
というフォーマット文字でした。
7.3. struct — 文字列データをパックされたバイナリデータとして解釈する — Python 2.7.14 ドキュメント
ただ、 2B2H4BH4B
が何を表しているのかよく分かりませんでした。
さらに調べてみたところ、以下の記事がありました。
python - struct.error: unpack requires a string argument of length 4 - Stack Overflow
struct.calcsize()を使えば何か分かるかなと思い、Suicaを置いて試してみたところ、
f = struct.calcsize('=2B2H4BH4B') print f # => 16
と表示されました。
16byteを表していることがわかったため、PythonのドキュメントとSuicaのフォーマットと比べてみました。
バイト | Pythonフォーマット | Suicaフォーマット |
---|---|---|
0 | B | 端末種 |
1 | B | 処理 |
2-3 | H | ?? |
4-5 | H | 日付 (先頭から7ビットが年、4ビットが月、残り5ビットが日) |
6 | B | 入線区 |
7 | B | 入駅順 |
8 | B | 出線区 |
9 | B | 出駅順 |
10-11 | H | 残高 (little endian) |
12-14 | 3B | 連番 |
15 | B | リージョン |
2B2H4BH4B
がSuicaフォーマットに一致していました。
では、KURURUの場合を調べてみます。
KURURUの履歴フォーマットは、Rubotoの時と同じく以下を参考にしました。ありがとうございます。
KURURU 履歴フォーマット | あたがわの日記
そのため、KURURUフォーマットをPythonフォーマットに当てはめてみます。
バイト | Pythonフォーマット | KURURUフォーマット |
---|---|---|
0-1 | H | 年月日 |
2 | B | 降車時刻 |
3-4 | H | 機番 |
5 | B | 降車時刻 |
6-7 | H | 乗車停留所 |
8-9 | H | 降車停留所 |
10 | B | 場所、種別 |
11 | B | 会社、割引 |
12-15 | I | 残高 |
となりました。
残高が4byte使っていたため、
- BやHと同じ、unsignedなCの型
- 標準の長さが4
なPythonフォーマットは unsigned int
だったため、 12-15byteのところは I
としました。
残高フォーマットは unsigned int
なのかを試してみたところ、
print self.row_be[8] #=> 1500
と表示されました。残高が取れているようです。
(date >> 9) & 0x7f
次は年月日の「年」を求めているところです。
date = row_be[3] (date >> 9) & 0x7f
まず、 >>
はシフト演算です。
6.8. シフト演算 (shifting operation) | 6. 式 (expression) — Python 3.6.3 ドキュメント
dateの値、および2進数に直した値、および右に9bitシフトした値を見たところ、以下でした。
print date # => 6988 print bin(date) # => 0b1101101001100 print date >> 9 # => 13 print bin(date >> 9) #=> 0b1101
次に、 0x7f
はPythonの16進数表記なので、2進数に直すと 1111111
です。
&
はANDなので、シフト演算の結果と 0x7f
とのANDを取ると、
0001101 1111111 ---------------------- 0001101
2進数の 0001101
は13なので、これに2000を加えれば年になりました。
同じように月を求めると、
print date >> 5 #=> 218 print bin(date >> 5) #=> 0b11011010
から 0x0f
とのANDにて
11011010 00001111 ---------------------- 00001010
1010
となりました。10進数に直すと10です。
日についても同様ですが、 (date >> 0)
はシフト演算をしないため実質 date & 0x1f
となります。
print date >> 0 #=> 6988 print bin(date >> 0) #=> 0b1101101001100
から 0x1f
とのANDにて
1101101001100 0000000011111 ---------------------- 0000000000001100
1010
となりました。10進数に直すと12です。
合わせると、2013年10月12日となり、正しい年月日が取得できました。
ServiceCode(service_code >> 6 ,service_code & 0x3f)
続いてはServiceCodeオブジェクトを生成しているところです。
service_code = 0x090f nfc.tag.tt3.ServiceCode(service_code >> 6 ,service_code & 0x3f)
ServiceCodeのコンストラクタでは、
- 第一引数は
サービスナンバー
- 第二引数は
サービス属性
です。
Suicaのサービスコード 0x090f
(2byte) なため、それぞれ当てはめてみると
- サービスナンバーは、上位10byte
- 6byte右へシフト
- サービス属性は、下位6byte
0x3f
は111111
なので、下位6byteを捨てる
となりました。
KURURUのサービスコードは 0x000f
なので、同じようにしてServiceCodeオブジェクトを生成すれば良さそうです。
KURURUを読む時に考えたこと
続いて、KURURUを読む時に考えたことをメモします。
時刻の算出について
Suicaとは異なり、KURURUには乗車時刻・降車時刻があります。
時刻については、
時刻は 0 時 0 分からの経過分を 10 で割った値が入っています.つまり,そこを 10 倍した値が 0 時 0 分からの経過分です.
とのことなので、編集が必要です。
self.row_be[3]
で取得する値は16進数表記のint型です。
そのため、これを16進数表記intから、10進数表記intに変換します。
今回はstr文字列に一度戻してからもう一度intにします。
# 16進のintを16進表現の文字列にする hex_time = hex(self.row_be[3]) print hex_time #=> 0x71 # 16進表現の文字列を10進数値にする int_time = int(hex_time, 16) print int_time #=> 113
得られた値は経過分の1/10なので10倍します。また、時間表現にするため、60で割ります。
これで商が時間、余りが分になります。
なお、Pythonでは商と余りを一度に求めるのに、divmod()関数が使えます。
divmod(a, b) | 2. 組み込み関数 — Python 2.7.14 ドキュメント
# 10倍 origin_time = int_time * 10 # 商と余りを求める hm = divmod(origin_time, 60)
divmod()関数は、 (商, 余り)
のタプルで値を戻すので、それを以下のフォーマットで時間表現にします。
'{hm[0]:02d}:{hm[1]:02d}:00'.format(hm=hm)
format()では、タプルは要素でのアクセス hm[0]
できます。
7.1.3.2. 書式指定例 | 7.1. string — 一般的な文字列操作 — Python 2.7.14 ドキュメント
また、02d
は、
0
: マイナス符号なし2
: 幅として、2桁d
: 10進の数値
となります。
7.1.3.1. 書式指定ミニ言語仕様 | 7.1. string — 一般的な文字列操作 — Python 2.7.14 ドキュメント
ヒアドキュメント時のインデント削除
今回、ヒアドキュメントを使って履歴を表示します。
ただ、ヒアドキュメントを使うとインデントも表示されてしまうのが問題です。
インデントを削除する方法を調べたところ、標準モジュールの textwrap
を使えば良さそうでした。
- textwrap.dedent(text) | 7.7. textwrap — テキストの折り返しと詰め込み — Python 2.7.14 ドキュメント
- Pythonのヒアドキュメント - Qiita
ソースコード全体
以上を踏まえたソースコードです。
コメントは省略しましたが、後述のGitHubのコードには記載してあります。
kururu_reader.py
# -*- coding: utf-8 -*- # 以下を参考にKURURUを読みました。m2wasabiさん、ありがとうございます。 # https://github.com/m2wasabi/nfcpy-suica-sample/blob/master/suica_read.py import struct import textwrap import nfc import nfc.tag.tt3 KURURU_SERVICE_CODE = 0x000f class HistoryRecord(object): def __init__(self, data): self.row_be = struct.unpack('>HBHBHHBBI', data) def is_empty(self): # 年月日がオールゼロの場合、履歴が無い空のレコードとみなす return not all([ self.fetch_year(), self.fetch_month(), self.fetch_day(), ]) def fetch_year(self): return (self.row_be[0] >> 9) & 0b1111111 def fetch_month(self): return (self.row_be[0] >> 5) & 0b1111 def fetch_day(self): return self.row_be[0] & 0b11111 def fetch_alighting_time(self): return self.format_time(self.row_be[1]) def fetch_machine_no(self): return self.row_be[2] def fetch_boarding_time(self): return self.format_time(self.row_be[3]) def fetch_boarding_stop(self): return self.row_be[4] def fetch_alighting_stop(self): return self.row_be[5] def fetch_place(self): result = { 0x05: '車内 ({})', 0x07: '営業所 ({})', 0x0E: '券売機 ({})', }.get(place, '不明 ({})') return result.format(hex(place)) def fetch_category(self): category = self.row_be[6] & 0b1111 result = { 0x00: '入金 ({})', 0x02: '支払 ({})', }.get(category, '不明 ({})') return result.format(hex(category)) def fetch_company(self): company = (self.row_be[7] >> 4) & 0b1111 result = { 0x00: '長電バス ({})', 0x03: 'アルピコバス ({})', }.get(company, '不明 ({})') return result.format(hex(company)) def fetch_discount(self): discount = self.row_be[7] & 0b1111 result = { 0x00: '入金 ({})', 0x01: 'なし ({})', }.get(discount, '不明 ({})') return result.format(hex(discount)) def fetch_balance(self): return self.row_be[8] def format_time(self, usage_time): hex_time = hex(usage_time) int_time = int(hex_time, 16) origin_time = int_time * 10 hm = divmod(origin_time, 60) return '{hm[0]:02d}:{hm[1]:02d}:00'.format(hm=hm) def connected(tag): sc = nfc.tag.tt3.ServiceCode(KURURU_SERVICE_CODE >> 6, KURURU_SERVICE_CODE & 0x3f) for i in range(0, 10): bc = nfc.tag.tt3.BlockCode(i, service=0) data = tag.read_without_encryption([sc], [bc, ]) history = HistoryRecord(bytes(data)) if history.is_empty(): continue result = """ Block: {history_no} 日付: {yyyy}/{mm}/{dd} 機番: {machine} 乗車時刻: {boarding_time} 乗車停留所: {boarding_stop} 降車時刻: {alighting_time} 降車停留所: {alighting_stop} 場所: {place} 種別: {category} 会社: {company} 割引: {discount} 残高: {balance:,}円 """.format( history_no=i + 1, yyyy=history.fetch_year() + 2000, mm='{:02d}'.format(history.fetch_month()), dd='{:02d}'.format(history.fetch_day()), machine=history.fetch_machine_no(), boarding_time=history.fetch_boarding_time(), boarding_stop=history.fetch_boarding_stop(), alighting_time=history.fetch_alighting_time(), alighting_stop=history.fetch_alighting_stop(), place=history.fetch_place(), category=history.fetch_category(), company=history.fetch_company(), discount=history.fetch_discount(), balance=history.fetch_balance(), ) print '-' * 30 print textwrap.dedent(result) def main(): with nfc.ContactlessFrontend('usb') as clf: clf.connect(rdwr={'on-connect': connected}) if __name__ == '__main__': main()
実行結果
実行してみたところ、Robutoの時と同じ内容になりました*2。
$ python kururu_reader.py ------------------------------ Block: 1 日付: 2015/01/30 機番: 3072 乗車時刻: 11:20:00 乗車停留所: 3136 降車時刻: 11:50:00 降車停留所: 1 場所: 車内 (0x5) 種別: 支払 (0x2) 会社: アルピコバス (0x3) 割引: なし (0x1) 残高: 1,500円 ------------------------------ Block: 2 日付: 2015/01/30 機番: 3039 乗車時刻: 08:20:00 乗車停留所: 1 降車時刻: 08:30:00 降車停留所: 3506 場所: 車内 (0x5) 種別: 支払 (0x2) 会社: アルピコバス (0x3) 割引: なし (0x1) 残高: 1,860円 ------------------------------ Block: 3 日付: 2013/10/12 機番: 3 乗車時刻: 19:20:00 乗車停留所: 0 降車時刻: 19:20:00 降車停留所: 0 場所: 券売機 (0xe) 種別: 入金 (0x0) 会社: 長電バス (0x0) 割引: 入金 (0x0) 残高: 2,160円 ------------------------------ Block: 4 日付: 2013/10/12 機番: 3058 乗車時刻: 18:50:00 乗車停留所: 3271 降車時刻: 19:00:00 降車停留所: 3379 場所: 車内 (0x5) 種別: 支払 (0x2) 会社: アルピコバス (0x3) 割引: なし (0x1) 残高: 1,160円 ------------------------------ Block: 5 日付: 2013/10/12 機番: 1002 乗車時刻: 13:00:00 乗車停留所: 1632 降車時刻: 13:00:00 降車停留所: 1422 場所: 車内 (0x5) 種別: 支払 (0x2) 会社: 不明 (0x1) 割引: なし (0x1) 残高: 1,330円 ------------------------------ Block: 6 日付: 2013/10/12 機番: 0 乗車時刻: 09:40:00 乗車停留所: 0 降車時刻: 09:40:00 降車停留所: 0 場所: 営業所 (0x7) 種別: 入金 (0x0) 会社: アルピコバス (0x3) 割引: 入金 (0x0) 残高: 1,500円
ソースコード
GitHubに上げました。 felica/kururu_reader.py
ファイルが今回のものです。
https://github.com/thinkAmi-sandbox/nfcpy-sample