以前、Android + RubotoにてFeliCaのKURURUの履歴を読んでみました。
Rubotoを使い、Androidで「長野市バス共通ICカード KURURU(くるる)」の履歴を読んでみた - メモ的な思考的な
そこで今回は Python2 + nfcpy でKURURUを読んでみます。
とはいえ、nfcpyでFeliCaを読む方法がよくわかりませんでした。
調べてみたところ、以下がとても参考になりました。ありがとうございます。
そこで、前者のリポジトリに含まれる 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ドキュメントを読むと、
というフォーマット文字でした。
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
と表示されました。
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]
と表示されました。残高が取れているようです。
(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
print bin(date)
print date >> 9
print bin(date >> 9)
次に、 0x7f
はPythonの16進数表記なので、2進数に直すと 1111111
です。
&
はANDなので、シフト演算の結果と 0x7f
とのANDを取ると、
0001101
1111111
----------------------
0001101
2進数の 0001101
は13なので、これに2000を加えれば年になりました。
同じように月を求めると、
print date >> 5
print bin(date >> 5)
から 0x0f
とのANDにて
11011010
00001111
----------------------
00001010
1010
となりました。10進数に直すと10です。
日についても同様ですが、 (date >> 0)
はシフト演算をしないため実質 date & 0x1f
となります。
print date >> 0
print bin(date >> 0)
から 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
0x3f
は 111111
なので、下位6byteを捨てる
となりました。
KURURUのサービスコードは 0x000f
なので、同じようにしてServiceCodeオブジェクトを生成すれば良さそうです。
KURURUを読む時に考えたこと
続いて、KURURUを読む時に考えたことをメモします。
時刻の算出について
Suicaとは異なり、KURURUには乗車時刻・降車時刻があります。
時刻については、
時刻は 0 時 0 分からの経過分を 10 で割った値が入っています.つまり,そこを 10 倍した値が 0 時 0 分からの経過分です.
KURURU 履歴フォーマット | あたがわの日記
とのことなので、編集が必要です。
self.row_be[3]
で取得する値は16進数表記のint型です。
そのため、これを16進数表記intから、10進数表記intに変換します。
今回はstr文字列に一度戻してからもう一度intにします。
hex_time = hex(self.row_be[3])
print hex_time
int_time = int(hex_time, 16)
print int_time
得られた値は経過分の1/10なので10倍します。また、時間表現にするため、60で割ります。
これで商が時間、余りが分になります。
なお、Pythonでは商と余りを一度に求めるのに、divmod()関数が使えます。
divmod(a, b) | 2. 組み込み関数 — Python 2.7.14 ドキュメント
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
を使えば良さそうでした。
以上を踏まえたソースコードです。
コメントは省略しましたが、後述のGitHubのコードには記載してあります。
kururu_reader.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