WebUSBでType2Tagを読む

備忘録です。

まずNFCについてですが、`Near Field Communication`の略称で、近距離無線通信と呼ばれる技術です。

通信距離は10cm程度となっています。詳しい情報は、業界標準化団体NFCフォーラムの設立メンバーでもあるSONYさんのページから確認できます。

また、NFCにはタグと呼ばれる仕様があります。

現在、タグの定義には5種類あり、Type1~Type5までとなっております。

タイプによって、基づいている仕様が異なっていたり、通信速度やメモリ容量など形式が異なっています。

ちなみに、Type3はFeliCaと呼ばれるSONYさんが開発した技術方式に基づいています。

今回Type2Tagに絞っているのは、以下の製品を用いる事と、上記で説明した通り、タイプ毎に方式が異なってくるためです。

[サンワサプライ NFCタグ(10枚入り) 白 MM-NFCT](https://www.amazon.co.jp/%E3%82%B5%E3%83%B3%E3%83%AF%E3%82%B5%E3%83%97%E3%83%A9%E3%82%A4-NFC%E3%82%BF%E3%82%B0-10%E6%9E%9A%E5%85%A5%E3%82%8A-%E7%99%BD-MM-NFCT/dp/B00GXSGL5G/ref=pd_sbs_147_t_0/357-6589690-8490016?_encoding=UTF8&pd_rd_i=B00GXSGL5G&pd_rd_r=ebfcbab4-601c-4610-9d20-272df2efe3e6&pd_rd_w=GQefz&pd_rd_wg=F6xS4&pf_rd_p=ca22fd73-0f1e-4b39-9917-c84a20b3f3a8&pf_rd_r=330KXKKBRTWMQPZ18XCR&refRID=330KXKKBRTWMQPZ18XCR&th=1)

また、当記事に載せるコードの大部分は下記のQiita記事を参考にしています。

[WebUSBでFeliCaの一意なIDであるIDmを読む](https://qiita.com/saturday06/items/333fcdf5b3b8030c9b05)

---

コード全体は以下のようになります。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>NFC Test</title>
</head>
<body>
   <button id="start">PaSoRi Setup</button>
   <p id="idm"></p>
</body>
<script src="pasori.js"></script>
</html>
```

```javascript:pasori.js
let startButton = document.getElementById('start');
let idmMessage = document.getElementById('idm');
async function sleep(msec) {
  return new Promise(resolve => setTimeout(resolve, msec));
}
async function send(device, data) {
  let uint8a = new Uint8Array(data);
  console.log(">>>>>>>>>>");
  console.log(uint8a);
  await device.transferOut(2, uint8a);
  await sleep(10);
}
async function receive(device, len) {
  console.log("<<<<<<<<<<" + len);
  let data = await device.transferIn(1, len);
  console.log(data);
  await sleep(10);
  let arr = [];
  for (let i = data.data.byteOffset; i < data.data.byteLength; i++) {
    arr.push(data.data.getUint8(i));
  }
  console.log(arr);
  return arr;
}

async function sendCommand (device, cmd, params) {
  let command = [0x00, 0x00, 0xff, 0xff, 0xff];
  let data = [0xd6, cmd].concat(params);
  command = command.concat([data.length, 0, 256 - data.length]);
  command = command.concat(data);
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    sum += data[i];
  }
  let parity = (256 - sum) % 256 + 256;
  command = command.concat([parity, 0]);
  await send(device, command);
  await receive(device, 6);
  const result = await receive(device, 40);
  return result;
}

async function session(device) {
  await send(device, [0x00, 0x00, 0xff, 0x00, 0xff, 0x00]);
  console.debug('GetProperty');
  await sendCommand(device, 0x2a, [0x01]);
  console.debug('SwitchRF');
  await sendCommand(device, 0x06, [0x00]);
  console.debug('InSetRF');
  await sendCommand(device, 0x00, [0x02, 0x03, 0x0f, 0x03]);
  console.debug('InSetProtocol');
  await sendCommand(device, 0x02, [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]);
  await sendCommand(device, 0x02, [0x01, 0x00, 0x02, 0x00, 0x05, 0x01, 0x00, 0x06, 0x07, 0x07]);
  console.debug('InCommRF:SENS');
  await sendCommand(device, 0x04, [0x36, 0x01, 0x26]);
  console.debug('InCommRF:SDD');
  await sendCommand(device, 0x02, [0x04, 0x01, 0x07, 0x08]);
  await sendCommand(device, 0x02, [0x01, 0x00, 0x02, 0x00]);
  const ssdRes = await sendCommand(device, 0x04, [0x36, 0x01, 0x93, 0x20]);
  const result = arrayToHexString(ssdRes.slice(15, 19));
  console.log(result);
  return result;
}

function arrayToHexString (arr) {
  if(arr.length > 0) {
    let result = '';
    for (let i = 0; i < arr.length; i++) {
      const val = arr[i];
      if (val < 16) {
        result += '0';
      }
      result += val.toString(16);
      idmMessage.innerText = "カードのIDm: " + result;
    }
    return result.toUpperCase();
  } else {
    return null;
  }
}

startButton.addEventListener('click', async () => {
  let device;
  try {
    device = await navigator.usb.requestDevice({'filters': [
      {'vendorId': 0x054c, 'productId': 0x06c1}
    ]});
    console.log("open");
    await device.open();
  } catch (e) {
    console.log(e);
    alert(e);
    throw e;
  }
  try {
    console.log("selectConfiguration");
    await device.selectConfiguration(1);
    console.log("claimInterface");
    await device.claimInterface(0);
    console.log(device);
    do {
      await session(device);
      await sleep(500);
    } while (true);
  } catch (e) {
    console.log(e);
    alert(e);
    try {
      device.close();
    } catch (e) {
      console.log(e);
    }
    throw e;
  }
});
```

startというidを持つボタンをクリックした時、最初にデバイスのセットアップをします。

`navigator.usb.requestDevice`メソッドを呼び出す事で、ブラウザからユーザーに対し利用するデバイスの選択を求められますが、この際filtersというオプションにベンダIDと製品IDを渡す事で利用するデバイスを絞る事ができます。

Macで自身に繋がっているUSBデバイスのベンダIDと製品IDを調べるには下記コマンドを実行してください。

`$ system_profiler SPUSBDataType`

この結果接続されているUSBデバイスが一覧で表示されます。その中から`RC-S380/S`の項目を探しましょう。これがSONY製PaSoRiのデバイス名です。

```
...
        RC-S380/S:

          Product ID: 0x06c1
          Vendor ID: 0x054c  (Sony Corporation)
          Version: 1.11
          Serial Number: 0658444
          Speed: Up to 12 Mb/s
          Manufacturer: SONY
          Location ID: 0x14600000 / 4
          Current Available (mA): 500
          Current Required (mA): 200
          Extra Operating Current (mA): 0
...(略)
```

上2行に書かれてある`Product ID`と`Vendor ID`がそれぞれ先程の`requestDevice`メソッドに与えるfiltersの中の要素になります。

プロダクトIDは変わる事があるのでベンダIDのみfiltersに指定することも可能です。その場合であれば接続されてる機器情報を調べずとも、filtersのvendorIdに`0x054c`と与えましょう。

---

上記2つのファイルを用意し、ローカルでホストします。

ホストしない(`file://`でアクセスしようとする)とUSBデバイスと接続できません。

ローカルでホストする方法はいくつかありますが、単純にコマンド一発実行するだけで手軽にできるのでPythonのhttpモジュールを用います。上記2ファイルを設置したディレクトリで下記コマンドを実行します。

`$ python3 -m http.server 8000`

立ち上がったら、ブラウザで`http://localhost:8000`にアクセスします。

<img width="587" alt="スクリーンショット 2020-01-03 18.39.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249725/0b59f10c-b9ed-0367-06a1-c67f1f782de1.png">

RC-S380/Sを選択し、接続をクリックするとブラウザとPaSoRiで通信を始めるので、NFCタグを読ませてみます。

下記画像のように、タグの中身が表示されていれば成功です。

<img width="183" alt="スクリーンショット 2020-01-03 18.40.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249725/bdffc0a7-bd94-e561-828c-2707d7a26998.png">

---

この方法はType２Tagを読む方法です。この上にNFC/FeliCa対応のスマホを載せてみたり、Suicaなどを載せてみると値が一意に定まらず、いろんな結果が表示されます。

FeliCaの一意なIDmを読みたい場合には、上述しましたが、当記事を書く際に参考にさせていただいた[こちらの記事](https://qiita.com/saturday06/items/333fcdf5b3b8030c9b05)でお試しください。