Raspberry PiとPaSoRiでICカードのIDを取得するnpmパッケージを作ってみた

以前に書いた[Raspberry PiとテレビをHDMI接続してテレビのON/OFFを確認する](http://qiita.com/mascii/items/132c064c30e321dea911)という記事では、テレビのON/OFF(STANDBY)状況をRaspberry Piから確認することができ、これを活用すれば電子工作不要で高齢者見守りデバイスを製作できることをお伝えしました。
具体的には、npmに[`node-cec`](https://www.npmjs.com/package/node-cec)というパッケージがあり、これをインストールするとNode.jsでHDMI-CECの信号を送受信できるというものでした。

ところが、SONYのICカードリーダー PaSoRi(RC-S380)を用いてRaspberry PiでカードIDを取得するには、[`nfcpy`](https://github.com/nfcpy/nfcpy)というPythonのライブラリを使うしかありません。
Node.jsからPaSoRi(RC-S380)を使うには、PythonとNode.jsを連携(標準入出力を用いてカードIDを含むJSONを受け取るなど)する必要があります。

<img width="471" alt="Python-to-Nodejs.png" src="https://qiita-image-store.s3.amazonaws.com/0/137045/cc743ef9-98f6-8925-132b-6c769c79cb95.png">

PaSoRi(RC-S380)でも、HDMIと[`node-cec`](https://www.npmjs.com/package/node-cec)のようにケーブルを接続してnpmパッケージをインストールするだけでICカードのIDを取得できればいいのにな... と思い、`nfcpy`を取り扱うためのPythonスクリプトを内包する [`node-nfcpy-id`](https://www.npmjs.com/package/node-nfcpy-id) というnpmパッケージを作ってみました。

<img width="480" alt="npm-node-nfcpy-id.png" src="https://qiita-image-store.s3.amazonaws.com/0/137045/4c86e785-b07a-28f3-4835-24ce662f1f9a.png">

# 動作環境
## Raspberry Pi側
- Raspberry Pi 3 Model B
    - Raspbian Jessie Lite 2017-04-10
        - イメージをMicroSDに書き込んだ後、MicroSDのルートディレクトリに`ssh`という名前で空のファイルを作成
            - 詳しくは[Raspberry PiとMac or Windows PCを有線で直接繋いでさくっとSSH接続する](http://qiita.com/mascii/items/7d955395158d4231aef6)などを参照

## ICカードリーダー
- SONY PaSoRi RC-S380
    - 2012年10月発売
    - 実売価格2500円前後

<img width="480" alt="P6110607.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/137045/89ffb640-2fdd-454d-d595-c00964cc1656.jpeg">

# カードIDとカードタイプについて
PaSoRi(RC-S380)で読み取れるNFCカードは、主にFeliCa規格のものとMIFARE規格のものがあります。
FeliCa規格ではカードIDのことをIDm、MIFARE規格ではカードIDのことをUIDといいます。
`node-nfcpy-id` では、カードIDを文字列(アルファベット小文字の16進数)で取得できます。
カード規格は `nfcpy` に準じた以下の数値(`2`, `3`, `4`)で取得できます。

- Type 2
    - MIFARE規格(DES Fire以外)のカード
    - ゲームセンターのカード(Aime, NESiCA)、国立国会図書館利用者カードなど
- Type 3
    - FeliCa規格のカード
    - Suicaなどの交通系、楽天Edy、nanaco、WAONなど
- Type 4
    - MIFARE DESFireのカード
    - au WALLETプリペイドカードなど

# インストール
## Node.jsのインストール
Node.jsとnpmの最新版を[Raspberry PiにNode.jsとnpmの最新版をインストールする](http://qiita.com/mascii/items/77c685df65c4cbca9315)を参照してインストールします

## `nfcpy`とPaSoRiを利用するための設定
Raspbianには標準でPython 2系がインストールされていますので、Python自体のインストールは不要です

apt-getで `python-usb` と `python-pip` をインストールします

```text:Raspbian
pi@raspberrypi:~ $ sudo apt-get install python-usb python-pip -y
```

pipで [`nfcpy-id-reader`](https://github.com/mascii/nfcpy_id_reader) (`node-nfcpy-id` と連携するPythonスクリプト)をインストールします。
`nfcpy-id-reader` をインストール際に `nfcpy` もインストールされます。

```text:Raspbian
pi@raspberrypi:~ $ sudo pip install -U nfcpy-id-reader
```

ユーザー権限でもPaSoRiを利用可能にするために、 `/etc/udev/rules.d/nfcdev.rules` というファイルを作成します

```text:Raspbian
pi@raspberrypi:~ $ cat << EOF | sudo tee /etc/udev/rules.d/nfcdev.rules
SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="06c3", GROUP="plugdev"
EOF
```

一度再起動をします

```text:Raspbian
pi@raspberrypi:~ $ sudo reboot
```

`node-nfcpy-id` を適当なディレクトリ上でインストールしてください

```text:Raspbian
pi@raspberrypi:~ $ npm install node-nfcpy-id --save
```

`npm install` 時に、 `npm ERR! 404 'types/node' is not in the npm registry.` のようなエラーが出る場合は、npmのバージョンが古い可能性があります。
npmの最新版を[Raspberry PiにNode.jsとnpmの最新版をインストールする](http://qiita.com/mascii/items/77c685df65c4cbca9315)を参照してインストールしてください。

# 使用方法
## 機能説明
### インポート
version 0.1.0より、CommonJSでのimport(`require`-ing)は下記のように `default` が必要になりました

```JavaScript
const NfcpyId = require('node-nfcpy-id').default;
```

TypeScript, ECMAScript 2015(Babel利用)で `import` 文を利用される方はこちら

```JavaScript
import NfcpyId from 'node-nfcpy-id';
```

### 初期化(モードの指定)
初期化時に動作モードを指定できます

#### `loop`モード
複数枚のカードを連続で読み込めるよう、カードリーダーをカード1枚ごとに停止させずに動作させます。
カードがカードリーダーから離れた時のイベントを取得できます。
コンストラクタの引数に何も指定しなかった場合のデフォルトのモードです。

```JavaScript
// const nfc = new NfcpyId({mode: 'loop'});
const nfc = new NfcpyId(); // loop mode
```

#### `non-loop`モード
カードを一度カードリーダーの上に置いて離すと、カードリーダーを停止させます。
カードがカードリーダーから離れた時のイベントを取得できます。

```JavaScript
const nfc = new NfcpyId({mode: 'non-loop'});
```

#### `non-touchend`モード
カードを一度カードリーダーの上に置くと、カードリーダーを停止させます。
カードがカードリーダーから離れた時のイベントを取得できません。

```JavaScript
const nfc = new NfcpyId({mode: 'non-touchend'});
```

### メソッド
#### `start()`メソッド
カードリーダーによるカードの読み取りを開始します

```JavaScript
nfc.start();
```

初期化と同時に`start()`メソッドを使用することもできます

```JavaScript
const nfc = new NfcpyId().start();
```

#### `pause()`メソッド
カードリーダーによるカードの読み取りを停止します

```JavaScript
nfc.pause();
```

### イベント
EventEmitterの形式でイベントを登録できます。

#### `touchstart`イベント
カードがカードリーダーの上に置かれた時に発生します。
コールバック関数の引数にカードIDとカードタイプが格納されます。

```JavaScript
nfc.on('touchstart', (card) => {
  console.log('touchstart', 'id:', card.id, 'type:', card.type);
});
```

#### `touchend`イベント
カードがカードリーダーから離れた時に発生します(`non-touchend`モードでは利用できません)

### ゲッター
#### `isRunning`プロパティ
カードリーダーが動作中の場合は `ture` を返し、そうでない場合は `false` を返します。

```JavaScript
console.log(nfc.isRunning); // true or false
```

### Node.jsの終了時
Node.jsのプロセスを終了(control+Cなども含む)する場合に、同時にPaSoRiから電波を発しない状態にしてからPythonの方もプロセスを終了します。

## 各モード・メソッド・イベントの図解

<img width="529" alt="node-nfcpy-id-modes.gif" src="https://qiita-image-store.s3.amazonaws.com/0/137045/11bedeeb-6778-e302-fdd3-87ea8c24faf4.png">

## サンプル
### `loop`モードの例
カードをカードリーダーの上に置くと、カードIDとカードタイプを出力し、カードがカードリーダーから離れた時に `touchend` を出力します

```JavaScript
const NfcpyId = require('node-nfcpy-id').default;
const nfc = new NfcpyId().start();

nfc.on('touchstart', (card) => {
  console.log('touchstart', 'id:', card.id, 'type:', card.type);
});

nfc.on('touchend', () => {
  console.log('touchend');
});

nfc.on('error', (err) => {
  // standard error output (color is red)
  console.error('\u001b[31m', err, '\u001b[0m');
});
```

#### 実行例

```text:Raspbian
pi@raspberrypi:~ $ node app
touchstart id: aa7dxxxx type: 2
touchend
touchstart id: 0114b3fxxxxxxxxx type: 3
touchend
touchstart id: 01103f0xxxxxxxxx type: 3
touchend
touchstart id: 0114b3fxxxxxxxxx type: 3
touchend
touchstart id: 04192xxxxxxxxx type: 4
touchend
```

### `non-touchend`モードの例
カードをカードリーダーの上に置くと、カードIDとカードタイプを出力し、5秒後にカードリーダーによる読み込みを再開します

```JavaScript
const NfcpyId = require('node-nfcpy-id').default;
const nfc = new NfcpyId({mode: 'non-touchend'}).start();

nfc.on('touchstart', (card) => {
  console.log('touchstart:', card.id, 'type:', card.type);
  console.log('5秒後に読み込みを再開します');
  setTimeout(() => {
    nfc.start();
  }, 5000);
});

nfc.on('error', (err) => {
  // standard error output (color is red)
  console.error('\u001b[31m', err, '\u001b[0m');
});
```

# 既知の問題
`loop`モードまたは`non-loop`モードの場合、Aimeなど一部MIFARE系カードをタッチすると`touchstart`イベント発生後、すぐに`touchend`イベントが発生し、`loop`モードの場合これを繰り返すチャタリングのような現象が発生します。
`nfcpy`をPythonだけで利用した場合も同様の現象が発生します。

# LT発表
このnpmパッケージについて、[2日間連続!!(1日目) We Are JavaScripters! @8th【初心者歓迎LT大会】](https://wajs.connpass.com/event/56952/)で「Node.js + Raspberry Piで お手軽IoT」([スライド](https://speakerdeck.com/mascii/node-dot-js-plus-raspberry-pite-oshou-qing-iot))というタイトルで発表いたしました
[<img width="480" alt="wejs-slide-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/137045/6a7f9802-e79a-b206-658b-5e878b0539f6.png">](https://speakerdeck.com/mascii/node-dot-js-plus-raspberry-pite-oshou-qing-iot)

# npmパッケージを作ってみて
- npmパッケージとして公開して使っていただくことを考えて、control+CでNode.jsを終了した場合などに同時にPythonの方も終了するような処理を書いてみました
- 誘導電流でLEDが発光するテスターのようなもの([共立プロダクツ KP-NFLEW2](http://prod.kyohritsu.com/KP-NFLEW2.html))を買ってみたところ、Pythonをうまく終了しないとPaSoRiが電波を発したままとなる(電源がオフにならない)ことがわかり、電源をオフにするために`nfcpy`のドキュメントやサンプルコードを見てうまくシグナルを送る必要がありました
    - シグナルを用いたためWin32に非対応となってしまった
    - Python単体で書くよりもPaSoRiをうまくコントロールできる気がします
- Pythonと連携する必要があったので、標準入出力の他にシグナルとコマンドライン引数を使ってしまい、コードが複雑になってしまいました
    - PythonとNode.jsを連携するという面倒な部分を吸収するという意味では、仕方なかったのかもしれない
- npmで公開することを意識して、ドキュメントやコミットログを(文法的に合っているのか別として)英語で書いてみました
    - ドキュメントを英語で作る難しさを改めて感じた
    - このQiitaの記事が日本語のドキュメントという感じになった
- We Are JavaScripters!でコーディングルールについてお話しされていた方がいらっしゃったのをきっかけに、ESLintを導入してみたところ、最初のインデントだけ半角スペース4個にしてしまうクセを見つけた(`git diff`が残念なことになった)

<img width="480" alt="KP-NFLEW2" src="https://qiita-image-store.s3.amazonaws.com/0/137045/f0e46c83-418c-38ee-d0eb-a653fd5062fc.jpeg">

# 参考サイト
[Raspberry piで、PaSoRi USB Felica Reader(RC-S380)を使用する](http://qiita.com/takanemu/items/1e6ef1207e5261040f05)
[3分でできるnpmモジュール](http://qiita.com/fnobi/items/f6b1574fb9f4518ed520)