Raspberry Pi + libpafe + Python + ctypesで、FeliCaのIDmを読む

手元に古いパソリがあったため、以下を参考に、Raspberry Piとパソリ RC-S320を使ってFeliCaを読んでみました。
Rasberry piでfelica(edy/suica)を読む - いろいろつまみ食い

 

環境

開発PC

 

RaspberryPi

 

FeliCa読み取り用ライブラリの調査

Pythonのライブラリを探してみたところ、nfcpyがありました。

 
とても良さそうでしたが、Supported Hardwareを確認したところ、

Sony RC-S330/360/370/380

http://nfcpy.readthedocs.org/en/latest/overview.html#supported-hardware

と、手元のRS-320はサポート外のようでした。

 
そのため、Pythonに限定しないでライブラリがないかを探してみたところ、libpafeがありました。
libpafe

上記の参考記事にもある通り、Raspberry Piでの動作も大丈夫そうだったので、libpafeを使うことにしました。

 
あとはlibpafeをどの言語から扱うかを考えたところ、libpafe-rubyがありました。
libpafe-ruby

Ruby2.1系でも動作しているようでした。
初心者歓迎詐欺被害者の会: Rubyでrequire 'pasori' したい話

 
ただRaspberry Piなので、Pythonで扱う方法がないかを調べたところ、Python標準ライブラリのctypesを使えばC言語のライブラリを扱えそうでした。
15.18. ctypes — Pythonのための外部関数ライブラリ — Python 2.7ja1 documentation

 
以上より、libpafe + ctypesでFeliCaを読むことにしました。

 

確認と準備

ディレクトリの作成

Raspberry Pi上にディレクトリを作っておきます。

pi@raspberrypi ~ $ ls
python_games
pi@raspberrypi ~ $ mkdir pasori
pi@raspberrypi ~ $ cd pasori

 

デフォルトのRaspbianでRC-S320が認識されているか確認

Raspbianを起動後、USBにRC-S320を挿入し、lsusbで状況を確認します。

pi@raspberrypi ~/pasori $ lsusb
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 054c:01bb Sony Corp. FeliCa S320 [PaSoRi]

デフォルトのままでも認識されているようです。

   

libpafeのインストール

ダウンロードと解凍
pi@raspberrypi ~/pasori $ wget http://homepage3.nifty.com/slokar/pasori/libpafe-0.0.8.tar.gz
...
2015-06-** **:**:** (703 KB/s) - `libpafe-0.0.8.tar.gz' saved [345264/345264]

pi@raspberrypi ~/pasori $ tar xzvf libpafe-0.0.8.tar.gz
...
libpafe-0.0.8/Makefile.in

 

インストール
デフォルトのままで./configure

パッケージが無いエラーが出ました。

pi@raspberrypi ~/pasori $ cd libpafe-0.0.8/
pi@raspberrypi ~/pasori/libpafe-0.0.8 $ ./configure
...
configure: error: Package requirements (libusb >= 0.1.12) were not met:

No package 'libusb' found

Consider adjusting the PKG_CONFIG_PATH environment variable if you installed software in a non-standard prefix.

Alternatively, you may set the environment variables LIBUSB_CFLAGS and LIBUSB_LIBS to avoid the need to call pkg-config.See the pkg-config man page for more details.

 

パッケージの確認

libusbの1.0系はdevがあるようですが、0.1系がdevがなく、これが原因でエラーが出たようです。

pi@raspberrypi ~/pasori $ sudo apt-cache search libusb*
...
libusb-0.1-4 - userspace USB programming library
libusb-1.0-0 - userspace USB programming library
libusb-1.0-0-dev - userspace USB programming library development files
...

 

libusbパッケージのインストール
pi@raspberrypi ~/pasori/libpafe-0.0.8 $ sudo apt-get install libusb-dev
...
Setting up libusb-dev (2:0.1.12-20+nmu1) ...

 

再度、./configure

成功しました。

pi@raspberrypi ~/pasori/libpafe-0.0.8 $ ./configure
...
config.status: executing libtool commands

 

make
pi@raspberrypi ~/pasori/libpafe-0.0.8 $ make
...
make[1]: Leaving directory '/home/pi/pasori/libpafe-0.0.8'

 

make install
pi@raspberrypi ~/pasori/libpafe-0.0.8 $ sudo make install
...
Libraries have been installed in:
   /usr/local/lib
...
make[1]: Leaving directory '/home/pi/pasori/libpafe-0.0.8'

 

インストール先の確認

Pythonctypesで使うlibpafe.soが見つかりました。

pi@raspberrypi /usr/local/lib $ ls
libpafe.a   libpafe.so    libpafe.so.0.0.8  python3.2
libpafe.la  libpafe.so.0  python2.7         site_ruby

 

動作確認

付属のプログラムを実行して、問題なく動作することを確認します。

pi@raspberrypi ~/pasori/libpafe-0.0.8 $ sudo ./tests/pasori_test
PaSoRi (RC-S320)
 firmware version 1.40
Echo test... success
EPROM test... success
RAM test... success
CPU test... success
Polling test... success

 

udevの設定

udevの設定を行います。

pi@raspberrypi ~ $ sudo nano /lib/udev/rules.d/60-libpafe.rules
pi@raspberrypi ~ $ udevadm control --reload-rules
root privileges required
pi@raspberrypi ~ $ sudo udevadm control --reload-rules

 
なお、/lib/udev/rules.d/60-libpafe.rulesに記載する内容は、改行も含めてlibpafeのページにもある通りにします。

ACTION!="add", GOTO="pasori_rules_end"
SUBSYSTEM=="usb_device", GOTO="pasori_rules_start"
SUBSYSTEM!="usb", GOTO="pasori_rules_end"
LABEL="pasori_rules_start"

ATTRS{idVendor}=="054c", ATTRS{idProduct}=="006c", MODE="0664", GROUP="plugdev"
ATTRS{idVendor}=="054c", ATTRS{idProduct}=="01bb", MODE="0664", GROUP="plugdev"
ATTRS{idVendor}=="054c", ATTRS{idProduct}=="02e1", MODE="0664", GROUP="plugdev"

LABEL="pasori_rules_end"

 
udev設定後に再起動し、sudoなしでも実行できることを確認します。

pi@raspberrypi ~ $ sudo reboot

# 再起動後
pi@raspberrypi ~ $ ./pasori/libpafe-0.0.8/tests/pasori_test
PaSoRi (RC-S320)
 firmware version 1.40
Echo test... success
EPROM test... success
RAM test... success
CPU test... success
Polling test... success

 

ctypesを使ったPythonスクリプトの作成

ディレクトリ作成
pi@raspberrypi ~ $ cd pasori
pi@raspberrypi ~/pasori $ mkdir py
pi@raspberrypi ~/pasori $ cd py

 

pasori_testをPythonで書く

pasori_testがlibpafeライブラリを使っていることもあり、FeliCaを読む前に、Python + ctypesでpasori_test.cを実装できるか試してみました。

なお、test関数の中でdata = c_char_p(TEST_DATA)の部分はPython3は動作しません。Python3ではc_char_pのかわりにc_wchar_pを使う必要があります。

 

pasori_test.py
# -*- coding: utf-8 -*-

# print時の改行を制御したいので、Python3.xのprint関数を使う
from __future__ import print_function
from ctypes import *


# PASORI_TYPE
# libpafe.hの17行目にenumとして定義
PASORI_TYPE_S310 = 0
PASORI_TYPE_S320 = 1
PASORI_TYPE_S330 = 2

TEST_DATA = "test data."


def test(libpafe, pasori):
    # Python2の場合
    data = c_char_p(TEST_DATA)
    # Python3の場合
    # data = c_wchar_p(TEST_DATA)

    data_length = c_int(len(TEST_DATA) - 1)

    print("Echo test...", end="")
    # Pythonの場合、数字の0はfalseとして扱われるので、
    # 関数が成功した場合はfalseが返ってくることになる
    if (libpafe.pasori_test_echo(pasori, data, byref(data_length))):
        print("error!")
    else:
        print("success")


    print("EPROM test... ", end="");
    if (libpafe.pasori_test_eprom(pasori)):
        print("error!")
    else:
        print("success")


    print("RAM test... ", end="");
    if (libpafe.pasori_test_ram(pasori)):
        print("error!")
    else:
        print("success")


    print("CPU test... ", end="")
    if (libpafe.pasori_test_cpu(pasori)):
        print("error!")
    else:
        print("success")


    print("Polling test... ", end="");
    if (libpafe.pasori_test_polling(pasori)):
        print("error!")
    else:
        print("success")


def show_version(libpafe, pasori):

    v1 = c_int()
    v2 = c_int()
    if (libpafe.pasori_version(pasori, byref(v1), byref(v2)) != 0):
        print("cannot get version")
        return

    # RC-S320以外は所持していないので、未テスト
    pasori_type = libpafe.pasori_type(pasori)
    if pasori_type == PASORI_TYPE_S320:
        print("PaSoRi (RC-S320)\n firmware version %d.%02d" % (v1.value, v2.value))
    elif pasori_type == PASORI_TYPE_S310:
        print("PaSoRi (RC-S310)\n firmware version %d.%02d" % (v1.value, v2.value))
    elif pasori_type == PASORI_TYPE_S330:
        print("PaSoRi (RC-S330)\n firmware version %d.%02d" % (v1.value, v2.value))
    else:
        print("unknown hardware.")
        

if __name__ == '__main__':

    libpafe = cdll.LoadLibrary("/usr/local/lib/libpafe.so")

    libpafe.pasori_open.restype = c_void_p
    pasori = libpafe.pasori_open()
    
    libpafe.pasori_init(pasori)
    show_version(libpafe, pasori)

    type = libpafe.pasori_type(pasori)

    if (type in {PASORI_TYPE_S310, PASORI_TYPE_S320}):
        test(libpafe, pasori)

    libpafe.pasori_close(pasori)

 

RaspberryPiへのファイル転送

前回同様、scpでファイルを転送します。

..\LibpafePi>scp -r * pi@192.168.0.21:~/pasori/py
pi@192.168.0.21's password:
LibpafePi.pyproj                              100% 2091     2.0KB/s   00:00
pasori_test.py                                100% 2514     2.5KB/s   00:00

 

Pythonスクリプトの実行

実行結果は同じとなり、libpafeがPython + ctypesで扱えることが分かりました。

pi@raspberrypi ~/pasori/py $ python pasori_test.py
PaSoRi (RC-S320)
 firmware version 1.40
Echo test...success
EPROM test... success
RAM test... success
CPU test... success
Polling test... success

 

felica_dump.cPythonで書くのは諦めた

同じサンプルコードとしてfelica_dump.cがありますが、元々のコード*1を追ってみたところ、

など、ctypesに慣れていない自分にはツラそうな内容だったため、今のところは諦めることにしました。

 

FeliCaIDmを読むPythonスクリプト

libpafeのREADMEを読んでみると、FeliCaIDmを読む関数がありました。これなら自分のctypes力でも書けそうでしたので、IDmを読むPythonスクリプトを書いてみました。

 

idm_reader.py
# -*- coding: utf-8 -*-

from __future__ import print_function
from ctypes import *

# libpafe.hの77行目で定義
FELICA_POLLING_ANY = 0xffff

if __name__ == '__main__':

    libpafe = cdll.LoadLibrary("/usr/local/lib/libpafe.so")

    libpafe.pasori_open.restype = c_void_p
    pasori = libpafe.pasori_open()
    
    libpafe.pasori_init(pasori)

    libpafe.felica_polling.restype = c_void_p
    felica = libpafe.felica_polling(pasori, FELICA_POLLING_ANY, 0, 0)

    idm = c_uint16()
    libpafe.felica_get_idm.restype = c_void_p
    libpafe.felica_get_idm(felica, byref(idm))

    # IDmは16進表記
    print("%04X" % idm.value)

    # READMEより、felica_polling()使用後はfree()を使う
    # なお、freeは自動的にライブラリに入っているもよう
    libpafe.free(felica)

    libpafe.pasori_close(pasori)

 

ファイル転送
..\LibpafePi>scp -r * pi@192.168.0.21:~/pasori/py
pi@192.168.0.21's password:
LibpafePi.pyproj                              100% 2091     2.0KB/s   00:00
idm_reader.py                                 100%  694     0.7KB/s   00:00
pasori_test.py                                100% 2514     2.5KB/s   00:00

 

実行

FeliCaをパソリの上に置いた上で実行してみると、Python + ctypesでも、felica_dumpを実行した結果と同じIDmを読むことができているようです。

# felica_dump版
pi@raspberrypi ~/pasori/libpafe-0.0.8/tests $ ./felica_dump
...
# Manufacture Code = 0101
...

# idm_reader.py版
pi@raspberrypi ~/pasori/py $ python idm_reader.py
0101

 

ctypesまわりのメモ

次回忘れそうなので、調べたことなどをまとめておきます。

 

ライブラリの読み込み
from ctypes import *

# 変数(ここではlibpafe)でライブラリを参照するため、
# これ以降は `libpafe.pasori_open` のようにしてライブラリの関数を呼び出せる
libpafe = cdll.LoadLibrary("/usr/local/lib/libpafe.so")

 

関数の戻り値がポインタ型の場合の、戻り値の受け取り方法

pasori_open関数はpsori型のポインタを返すことから、ポインタ型を受け取る必要があります。

この場合、関数名.restype (例:libpafe.pasori_open.restype)属性で事前に戻り値の型を設定することで、ポインタ型を受け取ることができるようです。
戻り値の型 - 15.18. ctypes — Pythonのための外部関数ライブラリ — Python 2.7ja1 documentation

# restype属性を使ってポインタ型を指定
libpafe.pasori_open.restype = c_void_p

# あとは普通に関数を呼び出して受け取る
pasori = libpafe.pasori_open()

 

関数の引数がポインタ型の場合の、関数への引数の渡し方法

pasori_test_echo関数は引数に3つのポインタを渡しています。

今回は、事前に変数の中にポインタを入れる方法(c_char_pなどを使う)と、関数の引数として渡すときにbyrefを使う方法の2つを試してみました。
ポインタを渡す(または、パラメータの参照渡し) - 15.18. ctypes — Pythonのための外部関数ライブラリ — Python 2.7ja1 documentation

# ***_p関数でポインタにする方法
data = c_char_p(TEST_DATA)

# c_***でC言語の型にして、引数として渡す際に`byref`を使う方法
data_length = c_int(len(TEST_DATA) - 1)

if (libpafe.pasori_test_echo(pasori, data, byref(data_length))):

 

PythonC言語の型について

基本的なものは以下の表にまとまっています。
基本のデータ型 - 15.18. ctypes — Pythonのための外部関数ライブラリ — Python 2.7ja1 documentation

なお、c_uint16などもある型の別名として用意されているようです。
基本データ型 - 15.18. ctypes — Pythonのための外部関数ライブラリ — Python 2.7ja1 documentation

 

C言語の型(c_***)から値を取り出す時

value属性を使います。
基本データ型 - value - 15.18. ctypes — Pythonのための外部関数ライブラリ — Python 2.7ja1 documentation

idm = c_uint16()
libpafe.felica_get_idm.restype = c_void_p
libpafe.felica_get_idm(felica, byref(idm))

print("%04X" % idm.value)

 

free関数について

felica_polling関数の説明には、

返された felica 型のポインタは不要になったときに free(3) で開放する必要がある。

と書かれていました。

ctypesの場合は、

後者のヘッダファイルがシステム上になければ、 "Python.h" が関数 malloc() 、 free() および realloc() を直接定義します。 http://docs.python.jp/2/extending/extending.html#extending-simpleexample

とのことなので、何もしなくてもライブラリにfreeが生えていたので、そのまま使うことができました。

libpafe.free(felica)

 

ctypesまわりの、その他参考

 

その他

 

ソースコード

PythonスクリプトGitHubに上げておきました。なお、libpafeのサンプルコードを参照して書いたため、ライセンスはlibpafeと同じGPL v2としました。
thinkAmi-sandbox/LibpafeRaspberryPi

*1:forkした(?)GitHubとlibpafe公式との間において、関係する部分では差がなさそうだったので、ブラウザから見れるGitHubのコードを使って例示しています