概要
Windows以外でもPaSoRiを使いたい!と思うと、真っ先に選択肢に上がるのがPythonのnfcpyだと思いますが、PythonでなくてJavaでやりたい!といったときに、サンプルコードが見つからなかった(というかほぼPythonのサンプルばかり・・・!!)ので動かすところまでをまとめてみたいと思います。
参考
先に参考を挙げておきますと、下記の記事を大いに参考とさせていただきました。仕様書等は法人として購入しないと手に入らないという中で、実装頂いた先人に感謝。
- 本家: https://github.com/nfcpy/nfcpy/blob/master/src/nfc/clf/rcs380.py
- C++での実装の大変有益な参考:https://qiita.com/ysomei/items/32f366b61a7b631c4750
- JavaScriptでの実装の大変有益な参考記事:https://qiita.com/saturday06/items/333fcdf5b3b8030c9b05
- NFC(Felica)について
JavaからUSBを扱う方法の簡単なまとめ
下記プロジェクトが、新しく、盛んなようなので、何も考えず採用します。
http://usb4java.org/
サイトを見ると、Low level API(libusb)とHigh Level API(javax.usb)があるようで、今回はよりわかりやすいだろうということで、javax.usbのほうを使ってみました。
この際、以下のページが非常に参考になります。
JSR-80 APIを使用してUSBデバイスにアクセスするための通常の手順は、以下のとおりです。
- UsbHostManagerから適切なUsbServicesを取得して、ブートストラップします。
- UsbServicesによってルートハブにアクセスします。ルートハブはアプリケーション中でのUsbHubと見なされます。
- ルートハブに接続されているUsbDeviceのリストを取得します。適切なUsbDeviceを検出するために、全ての下位層のハブを調べます。
- コントロールメッセージ(UsbControlIrp)を使用してUsbDeviceと直接通信するか、あるいはUsbDeviceの適切なUsbConfigurationからUsbInterfaceを求め、UsbInterfaceで利用可能なUsbEndpointを使用してI/Oを実行します。
- UsbEndpointがI/Oを実行するために使用される場合は、それに関連したUsbPipeを開きます。アップストリームデータ(USBデバイスからホストコンピューターへ)およびダウンストリームデータ(ホストコンピューターからUSBデバイスへ)は、UsbPipeによって同期あるいは非同期でサブミットされます。
- UsbPipeを閉じ、アプリケーションがもはやUsbDeviceへのアクセスを必要としない場合は適切なUsbInterfaceを解放します。
上記記事をふまえて、通信を行うまでの手順を簡単にまとめると
- ベンダーIDとプロダクトIDからUSBを検出する。それぞれのIDは一意で公開されている(はず)。例えば今回のRC-S380であれば、ベンダーIDは0x054C・プロダクトIDは0x06C3です。
- そこから、Configuration, Interface, Endpointを取得していく。なお、Endpointが実際にUSBと通信する際にデータのやりとりを行う場所で、USBに対して内向きと外向きの2つ存在する。
- 任意の通信をデバイスに対して送り、レスポンスを受け取る
以上の通りであり、デバイスへの制御コマンドさえ分かれば、そう難しい手順ではありません。
実践
デバイスの検出
USBはハブでツリー形式に繋いでいけるので、芋づる式にUSBを捜査していき、目的のUSBを探す必要があります。
仮想のルートは以下で取れます。
UsbServices services = UsbHostManager.getUsbServices();
UsbHub rootHub = services.getRootUsbHub();
続いて、先程調べたベンダーとプロダクトIDからデバイスを探します。ここはサンプルのとおりですね。途中にハブがあっても再帰的に探します。
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 = 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の詳しいコマンドや、データ構造については、最初に挙げた参考を参照してもらうのが手っ取り早いかと思いますので、ここでは割愛します。
ここから、デバイスに対し、
- ACK(戻りデータ無し)
- Set Command Type
- Get Firmware version
- Get PD DATA version
- Switch RF
- In Set RF
- 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
誤植や誤りがあれば容赦なく、ご指摘下さい。