はじめに
前回までの記事で紹介しました、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 とのデータ送受信とレスポンスデータの解析まで確認できました。
今回は、RC-S300 の communicationThruEX コマンドを使って、FeliCa カードへアクセスします。
便宜上 「FeliCa カード」との表記がありますが、物理的な FeliCa カード を指します。
「FeliCa Lite-S」は規格としての呼称としています。
ソニー株式会社 FeliCa 非接触ICカード 技術情報(PDFダウンロード) 「FeliCa Lite-Sユーザーズマニュアル」
注意事項
私自身 USBDevice API を使うのが初めてですし RC-S300 以外の機種を操作接続した経験がないので、他の機器でもここでの説明が当てはまるのかは保証できません。
自分なりの勝手な推論(あてずっぽう?)で、こんな感じかなでやっている部分も多々ありますので、参考程度に留めていただいて、ご自身で再検証されることをおすすめします。
USBDevice API の説明と 非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 の説明が混在している場合がありますので、ご了承下さい。
「JavaScript FeliCa Lite-S操作モジュール」で使っている USBDevice
USBDevice を使うとクライアントのUSBデバイスとペアリングすることができ、インターフェイスによりUSBデバイスを操作することができます。
MDN References > Web APIs > USBDevice
動作イメージ
クライアントPCのブラウザから、USBDevice を使用してUSB接続されている、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 を操作する。
USBDevice がコネクトするのはUSB接続されている SONY PaSoRi RC-S300 です。FeliCa カードへはSONY PaSoRi RC-S300 を経由してアクセスします。
USBに接続された機器の操作コマンドとしては、SONY PaSoRi RC-S300 を操作するためのコマンド ( TurnOffTheRF / TurnOnTheRF / StartTransparentSession / EndTransparentSession 等 ) と、FeliCa Lite-S を操作するための FeliCa コマンド (Polling / Read Without Encryption / Write Without Encryption 等 ) の2種類のコマンドがあります。FeliCa コマンドは RC-S300 の CommunicateThruEx コマンドにより ターゲットの FeliCa カードへ送信されます。
SONY PaSoRi RC-S300 communicateThruEX コマンドを使って FeliCa カードを操作
前回記事で、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 のコマンドをレスポンスを把握できているだけ説明しました。
今回は、その RC-S300 コマンドの communicateThruEX コマンドを使って、FeliCaカードを操作します。
communicateThruEX コマンドは、付属するデータをそのままターゲット( FeliCa カード )へ送ります。
communicateThruEX コマンドのデータとしては、FeliCa カード用のリクエストヘッダーの付いたコマンドが必要です。
communicateThruEX コマンド説明
communicateThruEX コマンドは、付属するデータをそのままターゲット( FeliCa カード )へ送ります。
そのため、コマンドのデータにFeliCa カード用のリクエストヘッダーが必要となります。
communicateThruEX | データをそのまま RFコマンドパケットとして ターゲットに送信 | 0xFF, 0x50, 0x00, 0x01, 0x00 |
communicateThruEX コマンドは、上記コマンドに続き 2 バイトのデータ長を設定します。
データの最後に 3 バイトの 0x00 を付加します。
FeliCa Lite-S カード リクエストヘッダー
FeliCa Lite-S カードリクエストには 11 バイトのリクエストヘッダーが必要です。
リクエストヘッダーは、0x5F, 0x46, 0x04 から始まる11 バイトのデータです。
タイムアウト時間は、多分マイクロ秒指定かと思われます(?)、リトルエンディアンの 4 バイトです。
データ長は、リクエストヘッダーに続くコマンドのデータ長を設定します。
FeliCa Lite-S コマンド
FeliCa Lite-S のコマンドについては、下記の資料を参考にしてください。
ソニー株式会社 FeliCa 非接触ICカード 技術情報(PDFダウンロード) 「FeliCa Lite-Sユーザーズマニュアル」4.4 コマンド仕様
FeliCa Lite-S コマンドで使えるコマンドは、Polling / Read Without Encryption / Write Without Encryption の 3 種類です。
コマンドコード | レスポンスコード | |
Polling | 00 | 01 |
Read Without Encryption | 06 | 07 |
Write Without Encryption | 08 | 09 |
今回は、communicateThruEX コマンドの動作を確認するだけですので、Polling コマンドだけを使って説明したいと思います。他 2 つのコマンドについては、後ほど紹介します「FeliCa Lite-S操作モジュール ArukasNFCLiteS」を参考にしてください。
Polling コマンド
Polling コマンドは、RC-S300 リーダー/ライターがカードを捕捉・特定するためのコマンドです。
FeliCa カードのシステム製造ID ( IDm ) と製造パラメータ ( PMm ) を取得できます。
リクエストコードの取得により FeliCa カードのシステムコードや通信性能を取得できます。
Polling コマンドは、コマンドコード 0x00 から始まる 4 バイトのコマンドです。
システムコードは、 システムコードを指定すると指定したシステムコードを持つ FeliCa カード だけ捕捉・特定することができます。0xFF はワイルドカードを表します。
リクエストコードは、0x00:要求なし・0x01:システムコード・0x02:通信性能を取得することができます。
タイムスロットは今回は考慮しません。
コマンドの先頭に1バイトのコマンド長 + 1 のコマンドレングスを付加しなければなりません。
Polling コマンドは 5 バイトのコマンドですので、コマンドレングス 6 を付加します。
リクエストとレスポンス
Polling のリクエストとレスポンスは以下のようになります。
SEND (Polling) --> [ 6B 1B 00 00 00 00 05 00 00 00 FF 50 00 01 00 00 11 5F 46 04 A0 86 01 00 95 82 00 06 06 00 FF FF 01 00 00 00 00 ] :: :: :: :: :: :: :: :: :: :: :: :: :: :: RECV Status[ok] <-- [ 83 24 00 00 00 00 05 02 00 00 C0 03 00 90 00 92 01 00 96 02 00 00 97 14 14 01 01 2E 4C E1 5C 90 B4 3F 00 F1 00 00 00 01 43 00 88 B4 90 00 ] :: :: :: :: :: :: :: :: -> <- :: ::
0x6B から始まるリクエストヘッダー、0xFF 0x50 0x00 0x01 0x00 ( communicateThruEX コマンド )、0x5F 0x46 0x04 から始まる FeliCa カードヘッダー、そして 0x00 0xFF 0xFF 0x01 0x00 ( Polling コマンド )、がリクエストされています。
レスポンスは前回説明した、0x83 から始まるレスポンスヘッダーと、0xC0 0x03 0x00 から始まるコマンドステータスが返ってきます。
FeliCa Lite-S コマンドのレスポンスは、0x97 のバイトの次のバイトが FeliCa Lite-S コマンドのレスポンスデータ長をあらわします。今回の場合ですと、0x14 = 20 バイトのデータがレスポンスデータ長に続いて、レスポンスデータが返ってきます。
[ 14 01 01 2E 4C E1 5C 90 B4 3F 00 F1 00 00 00 01 43 00 88 B4 ]
最初の 1 バイトはレスポンスデータ長、次の1バイトはレスポンスコードです。
Polling のレスポンスコードは 0x01 です。
FeliCa Lite-S カード操作コード
FeliCa カードを RC-S300 に置いて、「Execute」ボタンをクリックすると、カードのIDmとシステムコードが表示されます。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<head>
<title>USBDevice Test</title>
<style>
.textCenter { text-align: center ; }
.smfont { font-size: 0.7em; }
</style>
</head>
<body>
<h3>
<div class="header-title textCenter">
USBDevice Test
</div>
</h3>
<div class="mainArea">
<div class="button textCenter">
<button id="exe">Execute</button>
</div>
<br><br>
<div class="message">
<div id="message-title" class="message-title" style="display: inline;"></div><br>
<div id="message" class="message-info smfont" style="display: inline;"></div>
</div>
</div>
<script type='module'>
let messageTitle = document.querySelector( '#message-title' ) ,
message = document.querySelector( '#message' ) ,
exe = document.querySelector( '#exe' ) ,
usbDevice = '' ,
usbConfiguration = {} ,
seqNumber = 0 ;
const DeviceFilter = [
{ vendorId : 1356 , productId: 3528 }, // SONY PaSoRi RC-S300/S
{ vendorId : 1356 , productId: 3529 }, // SONY PaSoRi RC-S300/P
] ;
exe.addEventListener( 'click', async () => {
await usbDeviceConnect() ;
console.log( usbDevice, usbConfiguration ) ;
await usbDeviceOpen() ;
await felica() ;
await usbDeviceClose() ;
return;
});
// USB デバイスコネクト
var usbDeviceConnect = async () => {
const ud = await navigator.usb.getDevices() ; // ペアリング設定済みデバイスのUSBDeviceインスタンス取得
let peared = 0 ;
if ( ud.length > 0 ) {
for( let dev of ud ) {
const td = DeviceFilter.find( (fildev) => dev.vendorId == fildev.vendorId && dev.productId == fildev.productId ) ;
if ( td !== undefined ) {
++peared ;
usbDevice = dev ;
}
}
}
if ( peared != 1 ) {
usbDevice = await navigator.usb.requestDevice( { filters: DeviceFilter } ) ; // USB機器をペアリングフローから選択しデバイスのUSBDeviceインスタンス取得
}
usbConfiguration.confValue = usbDevice.configuration.configurationValue ;
usbConfiguration.interfaceNum = usbDevice.configuration.interfaces[ usbConfiguration.confValue ].interfaceNumber ; // インターフェイス番号
let ep = getEndPoint( usbDevice.configuration.interfaces[ usbConfiguration.confValue ] , 'in' ) ; // 入力エンドポイントを求める
usbConfiguration.endPointInNum = ep.endpointNumber ; // 入力エンドポイント
usbConfiguration.endPointInPacketSize = ep.packetSize ; // 入力パケットサイズ
ep = getEndPoint( usbDevice.configuration.interfaces[ usbConfiguration.confValue ] , 'out' ) ; // 出力エンドポイントを求める
usbConfiguration.endPointOutNum = ep.endpointNumber ; // 出力エンドポイント
usbConfiguration.endPointOutPacketSize = ep.packetSize ; // 出力パケットサイズ
return;
}
// USB デバイスオープン
var usbDeviceOpen = async() => {
message.innerHTML += '**OPEN<br/>' ;
await usbDevice.open() ; // USBデバイスセッション開始
await usbDevice.selectConfiguration( usbConfiguration.confValue ) ; // USBデバイスの構成を選択
await usbDevice.claimInterface( usbConfiguration.interfaceNum ) ; // USBデバイスの指定インターフェイスを排他アクセスにする
// RC-S300 コマンド
const endTransparent = [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x82, 0x00, 0x00 ] ;
const startransparent = [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x81, 0x00, 0x00 ] ;
const turnOff = [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x83, 0x00, 0x00 ] ;
const turnOn = [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x84, 0x00, 0x00 ] ;
const getSerialNumber = [ 0xFF, 0x5F, 0x03, 0x00 ] ;
let res ;
await sendUSB( endTransparent, 'End Transeparent Session' ) ;
res = await recvUSB( 64 ) ;
await sendUSB( startransparent, 'Start Transeparent Session' ) ;
res = await recvUSB( 64 ) ;
await sendUSB( turnOff, 'Turn Off RF' ) ;
await sleep( 50 ) ;
res = await recvUSB( 64 ) ;
await sleep( 50 ) ;
await sendUSB( turnOn, 'Turn On RF' ) ;
await sleep( 50 ) ;
res = await recvUSB( 64 ) ;
await sleep( 50 ) ;
// await sendUSB( getSerialNumber, 'Get SerialNumber' ) ;
// res = await recvUSB( 64 ) ;
return ;
}
// USB デバイスクローズ
var usbDeviceClose = async() => {
message.innerHTML += '**CLOSE<br/>' ;
// RC-S300 コマンド
const endTransparent = [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x82, 0x00, 0x00 ] ;
const turnOff = [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x83, 0x00, 0x00 ] ;
let res ;
await sendUSB( turnOff, 'Turn Off RF' ) ;
await sleep( 50 ) ;
res = await recvUSB( 64 ) ;
await sleep( 50 ) ;
await sendUSB( endTransparent, 'End Transeparent Session' ) ;
res = await recvUSB( 64 ) ;
await usbDevice.releaseInterface( usbConfiguration.interfaceNum ) ; // USBデバイスの指定インターフェイスを排他アクセスを解放する
await usbDevice.close() ; // USBデバイスセッション終了
return ;
}
// FeliCa 操作 ( communicateThruEX を使って FeliCa カードを操作する。 )
var felica = async () => {
// FeliCa Lite-S コマンド
const polling = [ 0x00, 0xFF, 0xFF, 0x01, 0x00 ] ; // ポーリング コマンド
const pollingCom = await usbDeviceCommunicationThruEX( polling ) ;
console.log( pollingCom ) ;
let res ;
await sendUSB( pollingCom, 'Polling' ) ;
res = await recvUSB( 64 ) ;
let resdata = await usbDeviceCommunicationThruEXResponse( res ) ;
console.log(resdata ) ;
if ( resdata.status === true ) {
resdata.IDm = resdata.data.slice(0,8) ;
resdata.PMm = resdata.data.slice(8,16) ;
resdata.systemCode = resdata.data.slice(16,18) ;
messageTitle.innerHTML = 'ポーリング成功:カードが見つかりました。<br/>IDm:' + arrayToHex( resdata.IDm ) + '<br/>システムコード:' + arrayToHex( resdata.systemCode ) ;
} else {
messageTitle.innerHTML = 'ポーリング失敗:カードが見つかりませんでした。' ;
}
}
// communicateThruEX を使って FeliCa カードを操作する。
var usbDeviceCommunicationThruEX = async( argCom ) => {
// RC-S300 コマンド communicateThruEX
const communicateThruEX = [ 0xFF, 0x50, 0x00, 0x01, 0x00 ] ;
// RC-S300 コマンド communicateThruEX フッター
const communicateThruEXFooter = [ 0x00, 0x00, 0x00 ] ;
// FeliCa リクエストヘッダー
const felicaHeader = [ 0x5F, 0x46, 0x04 ] ;
// FeliCa リクエストオプション
const felicaOption = [ 0x95, 0x82 ] ;
// タイムアウト(ms)
let felicaTimeout = 100 ;
// FeliCa Lite-S コマンドにレングスを付加
let felicaComLen = argCom.length + 1 ;
let felicaCom = [ felicaComLen, ...argCom ] ;
console.log( felicaCom ) ;
// FeliCa Lite-S リクエストヘッダーを付加
felicaTimeout *= 1e3 ; // マイクロ秒へ変換
let felicaReq = [ ...felicaHeader ] ; // リクエストヘッダー
felicaReq.push( 255 & felicaTimeout, felicaTimeout >> 8 & 255, felicaTimeout >> 16 & 255, felicaTimeout >> 24 & 255 ) ; // タイムアウト <<リトルエンディアン>> 4バイト
felicaReq.push( ...felicaOption ) ;
felicaReq.push( felicaComLen >> 8 & 255, 255 & felicaComLen ) ; // コマンドレングス
felicaReq.push( ...felicaCom ) ; // リクエストコマンド
// communicateThruEX コマンド作成
let felicaReqLen = felicaReq.length ;
let cTX = [ ...communicateThruEX ] ;
cTX.push( felicaReqLen >> 8 & 255, 255 & felicaReqLen ) ; // リクエストレングス
cTX.push( ...felicaReq ) ;
cTX.push( ...communicateThruEXFooter ) ;
return cTX ;
}
// communicateThruEX レスポンスデータを分解
var usbDeviceCommunicationThruEXResponse = async( argRes ) => {
let data = dataviewToArray( argRes.data ) ;
let retVal = { status : false } ;
// レスポンスデータ長の取得
let v = data.indexOf( 0x97 ) ; // レスポンスデータから 0x97 の位置を求める
if ( v >= 0 ) {
let w = v + 1 ; // 0x97 の次にデータ長が設定されている。(128バイト以上は未考慮です)
retVal.Length = data[ w ] ;
if ( retVal.Length > 0 ) {
retVal.allData = data.slice( w + 1, w + retVal.Length + 1 ) ; // 全レスポンスデータ切り出す
retVal.status = true ;
retVal.responseCode = retVal.allData[1] ; // レスポンスコード
retVal.data = retVal.allData.slice( 2, retVal.allData.length + 1 ) ; // レスポンスデータ(レングス、レスポンスコードを除いたデータ)
}
}
return retVal ;
}
// USBデバイスへデータを渡す
var sendUSB = async( argData, argProc = '' ) => {
const rdData = await addReqHeader( argData ) ;
await usbDevice.transferOut( usbConfiguration.endPointOutNum, rdData ) ;
const dataStr = arrayToHex( rdData ) ;
console.log( dataStr ) ;
message.innerHTML += 'SEND (' + argProc + ')<br/> --> [ ' + dataStr + ']<br/>' ;
}
// リクエストヘッダーの付加
var addReqHeader = ( argData ) => {
const dataLen = argData.length ;
const SLOTNUMBER = 0x00 ;
let retVal = new Uint8Array( 10 + dataLen ) ;
retVal[0] = 0x6b ; // ヘッダー作成
retVal[1] = 255 & dataLen ; // length をリトルエンディアン
retVal[2] = dataLen >> 8 & 255 ;
retVal[3] = dataLen >> 16 & 255 ;
retVal[4] = dataLen >> 24 & 255 ;
retVal[5] = SLOTNUMBER ; // タイムスロット番号
retVal[6] = ++seqNumber ; // 認識番号
0 != dataLen && retVal.set( argData, 10 ) ; // コマンド追加
return retVal ;
}
// USBデバイスからデータを受け取る
var recvUSB = async( argLength ) => {
const res = await usbDevice.transferIn( usbConfiguration.endPointInNum, argLength ) ;
const resStr = binArrayToHex( res.data ) ;
console.log( res ) ;
message.innerHTML += 'RECV Status[' + res.status + ']<br/> <-- [ ' + resStr + ']<br/>' ;
return res ;
}
// USBデバイス Endpoint の取得
var getEndPoint = ( argInterface, argVal ) => {
let retVal = false ;
for( const val of argInterface.alternate.endpoints ) {
if ( val.direction == argVal ) { retVal = val ; }
}
return retVal ;
}
// Dataviewから配列への変換
var dataviewToArray = ( argData ) => {
let retVal = new Array( argData.byteLength ) ;
for( let i = 0 ; i < argData.byteLength ; ++i ) {
retVal[i] = argData.getUint8(i) ;
}
return retVal ;
}
// DataViewの8ビットバイナリを16進数で返します。
var binArrayToHex = ( argData ) => {
let retVal = '' ;
let temp = [] ;
for ( let idx = 0 ; idx < argData.byteLength ; idx++) {
let bt = argData.getUint8( idx ) ;
let str = bt.toString(16) ;
str = bt < 0x10 ? '0' + str : str ;
retVal += str.toUpperCase() + ' ' ;
}
return retVal ;
}
// 配列の要素を16進数で返します。
var arrayToHex = ( argData ) => {
let retVal = '' ;
let temp = [] ;
for ( let val of argData ) {
let str = val.toString(16) ;
str = val < 0x10 ? '0' + str : str ;
retVal += str.toUpperCase() + ' ' ;
}
return retVal ;
}
// スリープ
var sleep = async (msec) => {
return new Promise(resolve => setTimeout(resolve, msec));
}
</script>
</body>
</html>
Android / IOS 端末を FeliCa カードとしてのポーリング時の注意
NFC機能付き Android / IOS 端末を FeliCa カードとしてポーリングする場合には、1回のポーリングでは捕捉・特定できない場合があります。
これは、端末側のNFC通信が Poll モードと Listen モードを切り替えながら通信相手を待ち受けしているからです。
端末は Poll モードで通信相手を補足すると、通信相手に対してリーダー/ライターとして動作します。(端末にカードを捕捉等)
Listen モードで通信相手を補足すると、カードとして動作します。(端末がリーダーを捕捉等)
このように、モードを切り替えながら待ち受けているため、ポーリングに対して端末が Poll モードの場合には捕捉・特定に失敗します。
※当コンテンツでは Android 端末でのみ実験しています。
詳細は次の資料を参考にしてください。
ソニー株式会社 FeliCa 非接触ICカード 技術情報(PDFダウンロード) 「NFC-FeliCa対応機器・アプリケーション開発時の注意事項」 3 Android OS における注意事項
最後に
これで、「JavaScript Web APIs USBDevice を使って FeliCa リーダー/ライターを操作してみました。」の説明を終わらせていただきます。
ここまで、お付き合い頂いたかた、ありがとうございます。
FeliCa Lite-S操作モジュール ArukasNFCLiteS
「FeliCa Lite-S操作モジュール ArukasNFCLiteS」の紹介
JavaScript からこのモジュールを利用すれば、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300へのコネクト・ポーリング・FeliCa Lite-S カードへの読み書きが簡単にできます。
動作確認は、
OS:Windows10 Pro・Windows10 Home
ブラウザ: Chrome・Edge
で行っています。
動作ハード確認は、Atom x5-Z8350 という低スペック PC でも動作しました。
ダウンロード
ダウンロードにあたり下記事項を確認してダウンロードしてください。
解凍後には必ず「JavaScript FeliCa Lite-S操作モジュール.txt」を一読し、免責・禁止事項・注意事項をご確認ください。交通系カードや電子マネーカードを使って、データが壊れたとかの責任は一切受け付けません。
・免責
当モジュール及びサンプルプログラムによるいかなる損害も、有限会社さくらシステム及び製作者はその責を一切負いません。
・禁止事項
有限会社さくらシステム及び製作者に許可なく、あらゆるメディア・コンテンツに再掲することを禁じます。
・注意事項
当モジュール及びサンプルプログラムは、技術検証を目的として作成されていますので、負荷試験を始めとする製品化を行うための基準を満たすための試験を一切行っておりませんので、予期せぬ結果を招く場合があります。
その場合であっても、上記免責事項により有限会社さくらシステム及び製作者はその責を一切負いません。
使用方法
ダウンロードした zip ファイルを解凍すると NFC フォルダが作成されますので、適当なWebサーバへアップロードしてください。
Web APIs USBDevice に対応したブラウザからサンプルへアクセスすることで、動作を確認することができます。
詳しくは、NFC フォルダ内の「JavaScript FeliCa Lite-S操作モジュール.txt」を参照してください。