LoginSignup
13
17

More than 5 years have passed since last update.

JavaでRC-S380(PaSoRi)を使ってFelicaを読んでみる

Last updated at Posted at 2018-11-20

概要

Windows以外でもPaSoRiを使いたい!と思うと、真っ先に選択肢に上がるのがPythonのnfcpyだと思いますが、PythonでなくてJavaでやりたい!といったときに、サンプルコードが見つからなかった(というかほぼPythonのサンプルばかり・・・!!)ので動かすところまでをまとめてみたいと思います。

参考

先に参考を挙げておきますと、下記の記事を大いに参考とさせていただきました。仕様書等は法人として購入しないと手に入らないという中で、実装頂いた先人に感謝。

JavaからUSBを扱う方法の簡単なまとめ

下記プロジェクトが、新しく、盛んなようなので、何も考えず採用します。
http://usb4java.org/

サイトを見ると、Low level API(libusb)とHigh Level API(javax.usb)があるようで、今回はよりわかりやすいだろうということで、javax.usbのほうを使ってみました。
この際、以下のページが非常に参考になります。
- JavaアプリケーションからUSBデバイスにアクセスする

JSR-80 APIを使用してUSBデバイスにアクセスするための通常の手順は、以下のとおりです。

  1. UsbHostManagerから適切なUsbServicesを取得して、ブートストラップします。
  2. UsbServicesによってルートハブにアクセスします。ルートハブはアプリケーション中でのUsbHubと見なされます。
  3. ルートハブに接続されているUsbDeviceのリストを取得します。適切なUsbDeviceを検出するために、全ての下位層のハブを調べます。
  4. コントロールメッセージ(UsbControlIrp)を使用してUsbDeviceと直接通信するか、あるいはUsbDeviceの適切なUsbConfigurationからUsbInterfaceを求め、UsbInterfaceで利用可能なUsbEndpointを使用してI/Oを実行します。
  5. UsbEndpointがI/Oを実行するために使用される場合は、それに関連したUsbPipeを開きます。アップストリームデータ(USBデバイスからホストコンピューターへ)およびダウンストリームデータ(ホストコンピューターからUSBデバイスへ)は、UsbPipeによって同期あるいは非同期でサブミットされます。
  6. UsbPipeを閉じ、アプリケーションがもはやUsbDeviceへのアクセスを必要としない場合は適切なUsbInterfaceを解放します。

上記記事をふまえて、通信を行うまでの手順を簡単にまとめると

  1. ベンダーIDとプロダクトIDからUSBを検出する。それぞれのIDは一意で公開されている(はず)。例えば今回のRC-S380であれば、ベンダーIDは0x054C・プロダクトIDは0x06C3です。
  2. そこから、Configuration, Interface, Endpointを取得していく。なお、Endpointが実際にUSBと通信する際にデータのやりとりを行う場所で、USBに対して内向きと外向きの2つ存在する。
  3. 任意の通信をデバイスに対して送り、レスポンスを受け取る

以上の通りであり、デバイスへの制御コマンドさえ分かれば、そう難しい手順ではありません。

実践

デバイスの検出

USBはハブでツリー形式に繋いでいけるので、芋づる式にUSBを捜査していき、目的のUSBを探す必要があります。
仮想のルートは以下で取れます。

RCS380.java
UsbServices services = UsbHostManager.getUsbServices();
UsbHub rootHub = services.getRootUsbHub();

続いて、先程調べたベンダーとプロダクトIDからデバイスを探します。ここはサンプルのとおりですね。途中にハブがあっても再帰的に探します。

RC380.java
public UsbDevice findDevice(UsbHub hub, int vendorId, int productId) throws UsbException, UnsupportedEncodingException {
        for (UsbDevice device : (List<UsbDevice>) hub.getAttachedUsbDevices()) {
            UsbDeviceDescriptor desc = device.getUsbDeviceDescriptor();

            if (desc.idVendor() == vendorId && desc.idProduct() == productId) return device;
            if (device.isUsbHub()) {
                device = findDevice((UsbHub) device, vendorId, productId);
                if (device != null) return device;
            }
        }
        return null;
    }

デバイスの検出は簡単ですね!

...と言いたかったのですが、ここで個人的にハマったポイントがあり、このまま進めていくとConfigurationがActiveでないため、2のEndpointでやりとりするためのUsbPipeがオープンできないという事態に陥りました。
javaxでsetConfigurationしたり、activateConfigurationするようメソッドがないか調べたのですが、どうも見つからず、後々のために、ここで一箇所だけLow Level API(libusb)を使ってConfigurationを設定しておきます。

//javax.usb.*だけだとConfiguration is not Activeとなるので、LibUSBでConfigurationをActive化する・・・。
DeviceHandle dh = LibUsb.openDeviceWithVidPid(null, (short) RCS380.VENDOR_ID, (short) RCS380.PRODUCT_ID);
LibUsb.setAutoDetachKernelDriver(dh, true);
LibUsb.setConfiguration(dh, 1);

UsbPipeまで一気に取得

ここから、芋づる式にUsbPipeまで一気に取得していきます。

RCS380.java
            rcs380 = this.findDevice(rootHub, RCS380.VENDOR_ID, RCS380.PRODUCT_ID);
            UsbConfiguration configuration = (UsbConfiguration) rcs380.getUsbConfigurations().get(0);
            this.iface = (UsbInterface) configuration.getUsbInterfaces().get(0);


            UsbEndpoint endpointOut = null, endpointIn = null;
            for (int i = 0; i < iface.getUsbEndpoints().size(); i++) {
                byte endpointAddr = (byte) ((UsbEndpoint) (iface.getUsbEndpoints().get(i))).getUsbEndpointDescriptor().bEndpointAddress();
                if (((endpointAddr & 0x80) == 0x80)) {
                    endpointIn = (UsbEndpoint) (iface.getUsbEndpoints().get(i));
                } else if ((endpointAddr & 0x80) == 0x00) {
                    endpointOut = (UsbEndpoint) (iface.getUsbEndpoints().get(i))
                    ;
                }
            }

            this.pipeOut = endpointOut.getUsbPipe();
            this.pipeIn = endpointIn.getUsbPipe();

ここでのポイントは、USBからホストへの向き(IN)のEndPointのアドレスの先頭4bitは0x8である、というルールがあるらしい、ということです。なので、0x80でANDをとって残っていればIN向き、そうでなければOUT向き、ということがわかります。

UsbPipeにデータをSubmitし、通信する

RC-S380の詳しいコマンドや、データ構造については、最初に挙げた参考を参照してもらうのが手っ取り早いかと思いますので、ここでは割愛します。
ここから、デバイスに対し、

  1. ACK(戻りデータ無し)
  2. Set Command Type
  3. Get Firmware version
  4. Get PD DATA version
  5. Switch RF
  6. In Set RF
  7. In Set Protocol

とコマンドを投げ続け、最後にPollingさせるコマンドを投げます。


 buf = rcs380.sendCommand(Chipset.CMD_GET_FIRMWARE_VERSION);
            System.out.println("Firmware version: " + String.format("%d.%02d", buf.get(1), buf.get(0)));

            buf = rcs380.sendCommand(Chipset.CMD_GET_PD_DATA_VERSION);
            System.out.println("PD Data version: " + String.format("%d.%02d", buf.get(1), buf.get(0)));

            rcs380.sendCommand(Chipset.CMD_SWITCH_RF, new byte[]{0x00});

            //0x01010f01 : F
            //0x02030f03 : A
            //0x03070f07 : B
            rcs380.sendCommand(Chipset.CMD_IN_SET_RF, new byte[]{0x01, 0x01, 0x0f, 0x01});
            rcs380.sendCommand(Chipset.CMD_IN_SET_PROTOCOL, new byte[]{0x00, 0x18, 0x01, 0x01, 0x02, 0x01, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x08, 0x08, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0e, 0x04, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x06});
            rcs380.sendCommand(Chipset.CMD_IN_SET_PROTOCOL, new byte[]{0x00, 0x18});

繰り返しになりますが、コマンドのコードやデータ構造は先人の方の記事や実装を見ていただくのが一番はやく確実かと思います・・・!

以下、Felicaを読み取っている部分です。

 System.out.println("********** Start **********");

            boolean isLoop = true;
            while (isLoop) {
                buf = rcs380.sendCommand(Chipset.CMD_IN_COMM_RF, new byte[]{0x6e, 0x00, 0x06, 0x00, (byte) 0xff, (byte) 0xff, 0x01, 0x00});
                if (Arrays.equals(buf.array(), new byte[]{(byte) 0x80, 0x00, 0x00, 0x00})) {

                } else {
                    //Type-F
                    if (buf.get(5) == 0x14 && buf.get(6) == 0x01) {
                        System.out.println("IDm: " + Hex.encodeHexString(Arrays.copyOfRange(buf.array(), 7, 15)));
                        System.out.println("PMm: " + Hex.encodeHexString(Arrays.copyOfRange(buf.array(), 15, 23)));
                        isLoop = false;
                    }
                }

                Thread.sleep(250);
            }

終わったら、pipeなどは閉じておきましょう。

rcs380.close();

以下、実行結果です。

/Library/Java/JavaVirtualMachi ... ...
SONY RC-S380/P
Firmware version: 1.17
PD Data version: 1.00
********** Start **********
IDm: 01120312eb18f200
PMm: 100b4b428485d0ff

Process finished with exit code 0

今回のソース

テスト目的なので汚いコードですが、以下にあります。
https://github.com/nearprosmith/java-rcs380-test

誤植や誤りがあれば容赦なく、ご指摘下さい。

13
17
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
17