C++
Mac
今更シリーズ
RC-S380
Libusb-1.0

今更ですが、SONY RC-S380 で Suica の IDm を読み込んでみた

SONY の RC-S380 を入手したので、とりあえず Suica の IDm を読み込んでみようとやってみました。
うちは Mac しかないので、Mac でも出来るかどうか確認もかねて。

libusb のインストール

まずはこれのインストールから。

$ brew install libusb

しかし、brew でインストールしてもなんかうまくいかなかったので、ソースから。

https://sourceforge.net/projects/libusb/files/libusb-1.0/libusb-1.0.21/

$ tar zxvf libusb-1.0.21.tar.bz2
$ cd libusb-1.0.21
$ ./configure
$ make
# make install

#brew でのインストールがうまくいかなかったのは、どうやら オブジェクトファイルが古いからみたいです。ソースからインストールすると最新になるので、それでいけるようになります。

#ちょっと忘れたので一応メモ。もしかしたら勝手になってるかも。。
#コンパイルのために /usr/local/include に brew でインストールしたやつのリンクを張っておきます。

# ln -s /usr/local/Cellar/libusb/1.0.21/include/libusb-1.0 /usr/local/include/libusb-1.0

デバイスの接続確認

まずはデバイスを認識するかどうかの確認です。
ここまではネット上にも情報があるので比較的簡単にできました。

devicecheck.cpp
/* usb test with libusb-1.0 */
#include <iostream>
#include "libusb.h"

int main() {
  libusb_device **devs;
  libusb_device *dev;
  libusb_device_descriptor desc;
  libusb_device_handle *dh;

  uint8_t path[8];
  uint8_t sdat[255];
  int r, cnt;

  r = libusb_init(NULL); // initalize
  if(r < 0) return r;

  cnt = libusb_get_device_list(NULL, &devs);
  if(cnt < 0) return cnt;

  for(int i = 0; i < cnt; i++){
    dev = devs[i];    
    r = libusb_get_device_descriptor(dev, &desc);
    if(r < 0){
      std::cout << "device get error..." << std::endl;
      return r;
    }

    // show device description
    printf("%04x/%04x (bus %d, device %d)", desc.idVendor, desc.idProduct,
       libusb_get_bus_number(dev), libusb_get_device_address(dev));

    r = libusb_get_port_numbers(dev, path, sizeof(path));
    if(r < 0) return r;
    printf(" path: %d", path[0]);
    for(int j = 1; j < r; j++){
      printf(".%d", path[j]);
    }

    // show spec string
    libusb_open(dev, &dh);
    // manufacturer, product, serialnumber 
    r = libusb_get_string_descriptor_ascii(dh, desc.iManufacturer,
                       (unsigned char *)sdat, sizeof(sdat));
    if(r > -1) printf(" %s", sdat);
    r = libusb_get_string_descriptor_ascii(dh, desc.iProduct,
                       (unsigned char *)sdat, sizeof(sdat));
    if(r > -1) printf(" %s", sdat);
    r = libusb_get_string_descriptor_ascii(dh, desc.iSerialNumber,
                       (unsigned char *)sdat, sizeof(sdat));
    if(r > -1) printf(" %s", sdat);

    libusb_release_interface(dh, 0);
    libusb_close(dh);

    printf("\n");
  }
  libusb_free_device_list(devs, 1);

  libusb_exit(NULL); // exit
  return 0;
}

で、コンパイルして実行してみます。

$ g++ devicecheck.cpp -I/usr/local/include/libusb-1.0 -L/usr/local/lib -lusb-1.0 -o devicecheck

$ ./devicecheck

054c/06c3 (bus 20, device 10) path: 1 SONY RC-S380/P 0577124
05ac/8406 (bus 20, device 8) path: 12 Apple Card Reader 000000000820
05ac/828f (bus 20, device 6) path: 3 Apple Inc. Bluetooth USB Host Controller
0a5c/4500 (bus 20, device 3) path: 3 Apple Inc. BRCM20702 Hub

はい、ちゃんと認識してますね。
最初の 054c/06c3 は前者が VENDOR_ID で 後者が PRODUCT_ID になります。
これは後々使うので覚えておきましょう。

コンパイルで苦戦したのは、-lusb-1.0 のところです。
普通に -lusb とやってもだめで、 -1.0 が必要というのがわかるまでちょっと苦労しました (^^;

USB について簡単に

libusb の関数を扱う時に少しUSBについて知っておかなければいけないことがあったので、調べてみました。

転送方法

転送方法には次の4つの方法があるようです。

  • コントロール転送
  • バルク転送
  • インタラプト転送(割り込み転送)
  • アイソクロナス転送(リアルタイム転送)

コントロール転送はすべてのデバイスが扱える転送方法です。
デバイスの情報取得や設定などを行う時に使うようです。データの送受信も可能です。

バルク転送は大量のデータを送受信する時に使います。

割り込み転送は、名前と違って実質はバルク転送のようなものです。データの受送信が発生した時に割り込み処理が発生する、ということはなくて、自分で監視する必要があるようです。

アイソクロナス転送は、カメラや音声など、高速大量の通信を行う時に使われるようです。チェックサム的なものがない分高速に転送が可能ということです。その代わり多少の間違いがあっても気にならない用途向けです。

エンドポイント

USBはデータの送受信を行うためにちょっとした場所を用意します。
その出入り口のことを「エンドポイント」と言うようです。

データをデバイスに送りたいときは、このエンドポイントが指す場所にデータを格納すると、すらすらと送られます。
逆にデバイスからのデータもエンドポイントが指す場所に格納されるので、そこをみに行くことでデータを取得します。
前者を ENDPOINT_OUT、後者を ENDPOINT_IN として区別します。

libusb では

libusb では、デバイス情報取得などで使用するコントロール転送は関数がカバーしてくれているので、あまり気にする必要はないみたいです。
#libusb_get_device_descriptor() など

バルク転送もインタラプト転送も関数があるので、特に問題なく利用できます。

バルク転送手順

デバイスをオープンして、そのデバイスのエンドポイント(IN、OUT)のアドレスを取得します。
あとは、取得したエンドポイントアドレスに対してデータの送受信を行います。

RC-S380 を制御する

Suica を読み込むためには、RC-S380 に対してデータの送受信をする必要があります。

デバイスの基本情報は、上記のソースコードで見ることはできますが、実際にはデバイスに対して制御コマンドを投げたりデータを取得したりする必要があります。

というわけで、SONY のホームページから各種資料をダウンロードして確認します。
https://www.sony.co.jp/Products/felica/business/tech-support/index.html

がしかし、これらはなんか古い情報のようで、RC-S380 では当てはまりませんでした。
というわけで、ネット上を探すのですが、出てくる情報は 「python を使って読み込んだ」というものばかりで、ちょっと求めているものと違うので、苦戦しました。

#RC-S380 は SONY NFC Port-100 chipset というのを使っているみたいです。
#このキーワードで検索すると ここ が見つかりましたが、これって linux カーネルのソース?

nfcpy の中身を見る

いろいろ探して見ましたが、python しかないので、諦めて中身をみてみることにしました。
https://github.com/nfcpy/nfcpy/blob/master/src/nfc/clf/rcs380.py

もうまさにこれ!これが欲しかった情報です!
というわけで、これを参考に C++ にしてみました。

Suica の IDm を取得する

ソースが長いみたいで Qiita に上げられなかったので Github に置いてます。
みてください。

https://github.com/ysomei/test_getnfcid/blob/master/getdeviceid.cpp

#Qiita に上げられなかったのは タグが5つ以上あったからみたいでした。。
#でもまあ Github に上げたので、このままにしておきます :)

get_usb_information

ここではデータを送受信するためのエンドポイントを取得しています。

packet_send

純粋にデータを送受信しているところです。
RC-S380 は コマンド送信 → ack/nack受信 → データ受信 という流れになっているので、受信を2回行なっているのがポイントです。

packet_write

コマンドを送信するためのパケットを作成して packet_send に送っています。
RC-S380 のパケットは、
ヘッダー + データ長さ + データ長さのチェックサム + データ + データのチェックサム + フッター
のようです。

ヘッダー: 0x00 0x00 0xff 0xff 0xff
データ長さ: コマンド長さ + 1
データ: コマンド列
フッター: 0x00

チェックサムは

 (0x100 - データの1バイト毎の合計 ) % 0x100

→ checksum()

あとコマンド列の先頭に 0xd6 を追加するのがポイントです。
データはこの 0xd6 も含めて計算します。

packet_init

RC-S380 を制御するために最初に投げるコマンドです。
#デバイスに対して ack を送信するみたいです。
#戻りはありません。

packet_xxx

以下はコマンドを投げるところです。
pakcet_init をしてから以下順に投げると良いです。

packet_setcommandtype
packet_switch_rf
packet_inset_rf
packet_inset_protocol_1
packet_inset_protocol_2
packet_sens_req

最後 packet_sens_req で受信データが取得できるので、それを解析して表示します。

main

引数をつけて Type-F(FeliCa) と Type-B(運転免許証など) の情報を読み取れるようにしてみました。
#ちなみに運転免許証の詳細情報はパスワードで守られてるのでこのソースでは見ることはできません。

実行してみる

というわけで、コンパイルして実行してみます。

$ g++ getdeviceid.cpp -I/usr/local/include/libusb-1.0 -L/usr/local/lib -lusb-1.0 -o getdeviceid 

まずは Suica(Type-F) から

$ ./getdeviceid -F

NFC Type-F scanning...
 IDm: 0114xxxxxxxx9305
 PMm: 1000xxxxxxxx0000

次に運転免許証(Type-B)

$ ./getdeviceid -B

NFC Type-B scanning...
 NFCID: 9xxxxxx4
 Application Data: cxxxxxx0
 Protocol Info: 7xxxxxx3

Type-A はコマンドつけて見ましたが、不完全なので、注意してください (^^

課題

Suica などの FeliCa はうまくいきました。
運転免許証も私のは読み取れましたが、他の人のをかざして見たらうまくいきませんでした。
ネット上では、発行した地域によって対象周波数が違う、みたいな記述もあるので、その辺りについても調べてみる必要がありそうです。

あと、時間があれば Type-A も出来るようにしたいですね。

参考

libusb関係

http://libusb.sourceforge.net/api-1.0/index.html

USB関係

http://www.picfun.com/usb02.html
http://www.kumikomi.net/archives/2007/03/22usb1.php

FeliCa関係

https://qiita.com/saturday06/items/333fcdf5b3b8030c9b05
https://github.com/nfcpy/nfcpy/blob/master/src/nfc/clf/rcs380.py