JavaScript Web APIs USBDevice を使って FeliCa リーダー/ライターを操作してみました。(その4:RC-S300コマンド)

JavaScript Web APIs USBDevice を使って FeliCa リーダー/ライターを操作してみました。(その1)
Web APIs USBDevice MDN のリファレンスを読んでいると、USBDevice なる Web API を見つけた。ブラウザから USB 機器へア…
sakura-system.com
JavaScript Web APIs USBDevice を使って FeliCa リーダー/ライターを操作してみました。(その2:USBデバイスへコネクト)
はじめに 前回の記事で紹介しました、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 を使って FeliCa Lite-S カードを操…
sakura-system.com
JavaScript Web APIs USBDevice を使って FeliCa リーダー/ライターを操作してみました。(その3:データの送受信)
はじめに 前回の記事で紹介しました、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 を使って FeliCa Lite-S カードを操…
sakura-system.com

はじめに

前回までの記事で紹介しました、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 を使って Web APIS USBDevice の簡単な使い方を説明しました。これにより、RC-S300 とのデータ送受信まで確認できました。

次は、何を RC-S300 へ送信し、どんなレスポンスが受信できるのかを知ることで、RC-S300 を操作できることができます。

今回は、RC-S300 のコマンドを、私が解析できたものを説明していきたいとおもいます。

ソニー株式会社 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 カードへ送信されます。


非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 リクエストとレスポンス

前回記事で、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 と transferOut / transferIn を使ってデータの送受信できるところまで説明しました。

JavaScript Web APIs USBDevice を使って FeliCa リーダー/ライターを操作してみました。(その3:データの送受信)
はじめに 前回の記事で紹介しました、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 を使って FeliCa Lite-S カードを操…
sakura-system.com

RC-S300 コマンド説明

私が、把握できた 非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 のコマンドです。
説明については、あくまでも推測の域を超えませんので、一部誤りがあるかもしれません。

GetFirmwareVersionファームウェアのバージョンを取得0xFF, 0x56, 0x00, 0x00
StartTransparentSessionトランスペアレントセッションの開始0xFF, 0x50, 0x00, 0x00, 0x02, 0x81, 0x00, 0x00
EndTransparentSessionトランスペアレントセッションの終了0xFF, 0x50, 0x00, 0x00, 0x02, 0x82, 0x00, 0x00
TurnOnTheRFRFのソフトパワーアップ0xFF, 0x50, 0x00, 0x00, 0x02, 0x84, 0x00, 0x00
TurnOffTheRFRFのソフトパワーダウン0xFF, 0x50, 0x00, 0x00, 0x02, 0x83, 0x00, 0x00
communicateThruEXデータをそのまま
RFコマンドパケットとして
ターゲットに送信
0xFF, 0x50, 0x00, 0x01, 0x00
GetDeviceTypeデバイスタイプを取得
1:Internal 2:External
0xFF, 0x5F, 0x08, 0x00
GetSerialNumberシリアル番号を取得0xFF, 0x5F, 0x03, 0x00

RC-S300 リクエストヘッダー

リクエストには 10 バイトのリクエストヘッダーが必要です。

リクエストヘッダーは、0x6B から始まる10バイトのデータです。
データ長は、リトルエンディアンの4バイト、リクエストヘッダーに続くコマンドのデータ長を設定します。
スロット番号は、タイムスロットを考慮しないので 0x00 で問題ないかと思います。
認識番号は、この数字がレスポンスヘッダーに付加されて戻りますので、自由に連番でも入れてください。

例えば、GetSerialNumber コマンドを使ってシリアル番号を取得する場合は。

0x6B, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x5F, 0x03, 0x00

となります。

リクエストヘッダーを付加する、ファンクション ( addReqHeader ) を追加したコードです。

<!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 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 = new Uint8Array( [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x82, 0x00, 0x00 ] ) ;
		const startransparent = new Uint8Array( [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x81, 0x00, 0x00 ] ) ;
		const turnOff = new Uint8Array( [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x83, 0x00, 0x00 ] ) ;
		const turnOn  = new Uint8Array( [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x84, 0x00, 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 ) ;

		return ;
	}

//	USB デバイスクローズ
	var usbDeviceClose = async() => {
		message.innerHTML += '**CLOSE<br/>' ;

		// RC-S300 コマンド
		const endTransparent = new Uint8Array( [ 0xFF, 0x50, 0x00, 0x00, 0x02, 0x82, 0x00, 0x00 ] ) ;
		const turnOff = new Uint8Array( [ 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 ;
	}

//	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/>&nbsp;&nbsp;&nbsp;--> [ ' + 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/>&nbsp;&nbsp;&nbsp;<-- [ ' + 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>

オープン・クローズ処理に設定された、RC-S300 向けコマンドからはヘッダーが除かれています。
結果はリクエストヘッダーが、付加されて正常にリクエストコマンドが実行されています。

RC-S300 レスポンスヘッダーとフッター

レスポンスデータには、10 バイトのレスポンスヘッダーと 2 バイトのレスポンスフッターが付加されます。

レスポンスヘッダー

レスポンスヘッダーは、0x83 から始まる10バイトのデータです。
データ長は、リトルエンディアンの4バイト、レスポンスヘッダーに続くレスポンスフッターを含んだレスポンスデータ長です。
認識番号は、リクエストヘッダーに付加された値が戻ります。

レスポンスフッターは、ステータスを表します。正常値は 0x90, 0x00 です。

RC-S300 レスポンスデータとコマンドステータス

リクエストコマンドが GetSerialNumber 等のように値を返すコマンドのレスポンスデータは、レスポンスヘッダーに続いてそのまま値が返ります。

SEND (Get SerialNumber)
   --> [ 6B 04 00 00 00 00 05 00 00 00 FF 5F 03 00 ]
                                       :: :: :: ::
RECV Status[ok]
   <-- [ 83 09 00 00 00 00 05 02 00 00 30 30 34 31 30 38 37 90 00 ]		// シリアル番号 0041087 を取得
                                       :: :: :: :: :: :: :: 

リクエストコマンドが TurnOffTheRF 等のように RC-S300 に何らかのアクションを求めるコマンドは、レスポンスヘッダーに続いて 0xC0, 0x03 から始まるコマンドステータスが返ります。
ただ、このコマンドステータスがどのようなエラーを指しているのかは、わかりませんが、正常時は必ず 5 バイトの以下の並びのデータが返ってきます。

SEND (Turn Off RF)
   --> [ 6B 08 00 00 00 00 03 00 00 00 FF 50 00 00 02 83 00 00 ]
                                       :: :: :: :: :: :: :: ::
RECV Status[ok] 
   <-- [ 83 07 00 00 00 00 03 02 00 00 C0 03 00 90 00 90 00 ]
                                       :: :: :: :: ::

コマンド実行失敗例としてクローズ処理の TurnOffTheRF を EndTransparentSession の後に実行すると、コマンドステータスは以下のようになります。

SEND (Turn Off RF)
   --> [ 6B 08 00 00 00 00 07 00 00 00 FF 50 00 00 02 83 00 00 ]
RECV Status[ok]
   <-- [ 83 07 00 00 00 00 07 02 00 00 C0 03 01 63 01 90 00 ]
                                       :: :: :: :: ::

communicateThruEX コマンドについては、この 5 バイトに続いて 0x92, 0x01 や 0x96, 0x02 等が続き、FeliCa Lite-S コマンドの結果は 0x97 に続く 1バイトのデータ長を示すデータの後に、コマンド実行結果が返されます。

RC-S300 コマンド のまとめ

これで、RC-S300コマンド のおおまかな利用方法は理解できたでしょうか。
とにかく、資料がなくて通信内容からなんとなく推測するのが精一杯で、「~だとおもいます」的な表現が多くなってきました。

流れとしては

1

USBDevice.open()

● USBデバイスセッション開始

2

EndTransparentSession

● トランスペアレントセッションの終了

セッションが終了されずに中断された等の対策のため。

3

StartTransparentSession

● トランスペアレントセッションの開始

4

TurnOffTheRF

● RFのソフトパワーダウン

5

TurnOnTheRF

● RFのソフトパワーアップ
メモリ・バッファのクリア

6

communicateThruEX

● FeliCa Lite-S カードにコマンドを送信

7

TurnOffTheRF

● RFのソフトパワーダウン

8

EndTransparentSession

● トランスペアレントセッションの終了

9

USBDevice.close()

● USBデバイスセッション終了

という感じです。

次回

次回は、非接触ICカードリーダー/ライター SONY PaSoRi RC-S300 の communicateThruEX コマンドを使って、いよいよ FeliCa Lite-S カードの操作を説明する予定です。

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」を参照してください。