【Electron】Felica連携デスクトップアプリケーションの開発

# 2020/03/27追加更新
ffiやrefのことも含め、Electronもアップデートが入り情報が古くなってしまっていたため最新の動作環境にアップデートしなおしました。


# 【Electron】Felica連携デスクトップアプリケーションの開発


この記事では以下の内容を含んでいます

* Node.jsのインストールとElectronのスタートアップ
* Electronでffiをインストール
* ffiでFelicalib.dllを起動し、PaSoRiからカードデータを取得する
* 作成したElectronアプリケーションをasarで１つのファイルにまとめ、パッケージングする。

少々長いですがお付き合いいただけたら幸いです。

# 前文
この記事はNode.jsを利用して簡単にちょっと手の込んだデスクトップアプリケーションを利用しよう。という記事です。
手元の環境の都合上Windows10のみでの話となっております。
また、その他アプリケーションの環境などは記事投稿時点（2020/03/27）のものとなっているため、もしも新しいバージョンなどが出た場合はこの記事が古くなっている可能性もあるので注意してください。

# Node.jsのインストールとElectronスタートアップ
[Electron公式ホームページ](https://electronjs.org/)によると現在の安定板は

```
$ npm i -D electron@latest
# Electron   8.2.0
# Node       12.13.0 (32bit版)
# Chromium   80.*
```
とのことなので、まずはNode.jsの指定バージョンを入手します。

## Node.jsのインストールと設定まで

[Node.jsのリリース一覧ページ](https://nodejs.org/ja/download/releases/)で該当のバージョンを探してください。

拡張子が`.msi` 。またFelicalibは32ビット対応のものなので`x86`のものをダウンロード。
この記事の場合は`node-v12.13.0-x86.msi`になります。

ダウンロード後、インストール。全部画面に沿ってインストールを完了させてください。

その後、ターミナルで

```
$ node -v
```
とやり、正常にバージョンが表示されればインストール完了です。

## Electronをインストールする

プロジェクトを作成後、そのプロジェクト内で初期化を行います。

```
your-project> npm ini
```

色々聞かれるが、すべてエンターでも大丈夫。気になる人は後程 `package.json` を編集してください。
そしてElectronのインストール。

```
your-project> npm install electron --save-dev
```

その後、package.jsonを以下のように編集

```json:package.json
{
  "name": "your-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "electron ."
  },
  "author": "",
  "license": "ISC",
  "dependencies": {},
  "devDependencies": {
    "electron": "^8.2.0"
  }
}
```
index.jsとindex.htmlも記述します。

```javascript:index.js
const electron = require('electron');
const {app, BrowserWindow} = electron;
const path = require('path');
const url = require('url');
const fs = require('fs');

let mainWindow;

//アプリケーション（基幹部）の起動が終了後動作する。
app.on('ready', () => {
	createWindow();
});

function createWindow() {
	mainWindow = new BrowserWindow({
		width: 400,
		height: 300,
		useContentSize: true,
		title: "test",

	});

	mainWindow.loadURL(url.format({
		pathname: path.join(__dirname, 'index.html'),
		protocol: 'file:',
		slashes: true
	}));

	//窓全部閉じたときnullにする
	mainWindow.on('closed', () => {
		mainWindow = null
	});
}
```

```html:index.html
<!Doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>はろーわーるど</title>
</head>
<body>
はろーわーるど
</body>
</html>
```

コマンドを実行して、問題なく動作することを確認。

```
your-project> npm start
```

# ffiのインストール

## ffiって何よ
ffiとは、正確にはnode-ffiというdllファイルを動作させるためのNode.jsモジュールです。
詳しくは以下のサイトのほうが詳しいのでどうぞ
[node-ffi/node-ffi](https://github.com/node-ffi/node-ffi)

## windows-build-toolsをインストールする。
windowsでffiをインストールするためのnode-gypを動作させるためには、pythonやVisualC++やらが必要になっている。
非常に面倒くさいのでそれらを一括で設定したりなんだりをやってくれるツールが存在しています。

[windows-build-tools](https://github.com/felixrieseberg/windows-build-tools)

ただし、**これは管理権限を付与したPowerShell**でないと実行できません。
PowerShellを起動し、以下のコマンドを入力

```
>> npm --add-python-to-path install --global windows-build-tools
```
この時、`--add-python-to-path`を忘れるとpythonへパスが通らずに、やっぱりnode-gypが失敗することがあります。忘れないように注意。

インストールが終了後、プロジェクトに戻って改めてバージョンを確認してください。

```
your-project> python -V
```

きちんとバージョンが表示されれば成功です。

また、どうしてもだめな時は以下のページを参考にするといいでしょう。（勝手に参考にしています。ありがとうございます）
[Windowsでnpm installしてnode-gypでつまずいた時対処方法](https://qiita.com/AkihiroTakamura/items/25ba516f8ec624e66ee7)

## ffiをインストールする
しかしffi公式はNode8以降対応していないため、別のものを導入します。
[ffi-napi](https://github.com/node-ffi-napi/node-ffi-napi)

```
your-project> npm install ffi-napi
```

また、`ffi`で利用するために

```
your-project> npm install ref-napi
```

あたりも入れておきましょう。

## 確認
それでは、index.jsで`ffi`と`ref`を読み込んでみましょう。

```javascript:index.js
const ffi = require('ffi-napi');
const ref = require('ref-napi');
```

そして実行。
**もちろん失敗します。**
以下のようなエラーが出るはずです。

```
App threw an error during load
Error: A dynamic link library (DLL) initialization routine failed.
\\?\C:\Users\your-project\node_modules\ref\build\Release\binding.node
    at process.module.(anonymous function) [as dlopen] (ELECTRON_ASAR.js:172:20)
    at Object.Module._extensions..node (module.js:598:18)
    at Object.module.(anonymous function) [as .node] (ELECTRON_ASAR.js:172:20)
    at Module.load (module.js:503:32)
    at tryModuleLoad (module.js:466:12)
    at Function.Module._load (module.js:458:3)
    at Module.require (module.js:513:17)
    at require (internal/module.js:11:18)
    at bindings (C:\Users\your-project\felicatest\node_modules\bindings\bindings.js:76:44)
    at Object.<anonymous> (C:\Users\your-project\felicatest\node_modules\ref\lib\ref.js:5:47)

```

そもそもElectronで使うようにネイティブアプリをビルドする必要があるのです。
**面倒ですね。**でも動くようにしましょう。

※この時、アプリケーションが中途半端に実行状態にある状態で落ちている場合が多いので`Ctrl+C`とか、タスクランナーとかできちんとアプリケーションを落としておくことをおすすめします。

## ffiを動くようにする。
Electronでffiを利用できるように、ビルド用のモジュールをインストールします。

```
your-project> npm install --save-dev electron-rebuild
```

インストール後、リビルドを実行します。

```
your-project> .\node_modules\.bin\electron-rebuild.cmd
```
ディレクトリを示すスラッシュはただのスラッシュでもバックスラッシュでもどちらでも問題ありません。ただ、バックスラッシュを利用するとWindows側できちんとディレクトリを認識してtabキーによる入力補完が使えるため非常に便利です。

## もう一度確認

それではもう一度`ffi`と`ref`を読み込んだスクリプトのあるelectronを実行しましょう。
**動いたら成功です。**

# ffiでFelicalib.dllを起動し、PaSoRiからカードデータを取得する
[felicalib公式サイト](http://felicalib.tmurakam.org/)

## FeliCaポートソフトウェアをインストールする
felicalib.dllはPaSoRi前提の仕組みのため、それを動作させるための前提アプリケーションと物理的に読み込むものが必要とされます。
[SONYのPaSoRi公式ページ](https://www.sony.co.jp/Products/felica/consumer/index.html?j-short=pcrw)から前提アプリケーションをダウンロードし、インストールしてください。
ついでにPaSoRi本体は何らかの手段で手に入れてください。Amazonとか、近所の電気屋とかで。

インストーラーにそってインストールを行ってください。
以降の記述はパソコンにPaSoRiがささっている前提で話しています。

## ffiでfelicalib.dllを読み込む
先ほどのfelicalib公式サイトからfelicalib.dllをダウンロードし、プロジェクトディレクトリ内に設置してください。
後程asarで圧縮する予定のため、ルートディレクトリに置いておくと問題が少なく済みます。

<font color=gray>どうしてもルートディレクトリ以外に設置したい場合は、がんばってasarで圧縮しても問題なく動作する方法を探すか、そもそも圧縮を諦めたほうが良いかもしれません。問題なく動作する方法を見つけたら是非とも私まで教えてください……。</font>

それでは、実際にfelicalibをffiで読み込みます・・・が、専用処理が多いのでライブラリに分割します。

```javascript:FelicaAccesser.js
const ref = require("ref-napi");
const ffi = require("ffi-napi");
const path = require("path");

/*
 * Felicalib.dllと連携し、PaSoRiを利用してIDmを読み取る仕組み
 */
let FelicaAccessar = {
	felicalib: null,
	pasoriPtr: null,
	felicap: null,
	isTouching: false,
	errorFlag: false, //エラー検出フラグ。検出した1回だけcallbackを出す
	/**
	 * FelicaAccesserを読み出し時、必ず一回動作させること。
	 * 念のため二回目以降はこのモデル内の変数を確認し、実行自体を行わないようにしている。
	 * @constructor
	 */
	Felica: () => {
		//felicalib.dllを利用できる形にコンバート
		//ポインタ型は ref.refType()で厳密に作成することも出来るが、別にchar*でもpointerでも良い。
		//（どう宣言しようとも、どうせNodejs内での扱いはBuffer型になる）
		//また、それぞれの利用する関数は以下のように宣言している。
		//関数名: [戻り値, [引数１,引数２,...]]
		if (FelicaAccessar.felicalib === null) {
			let intPtr = ref.refType("int");
			let stringPtr = ref.refType(ref.types.CString);

			try {
				let felicalibPath = path.join('./','felicalib');
				console.log(felicalibPath);
				FelicaAccessar.felicalib = ffi.Library(felicalibPath, {
					pasori_open: [intPtr, [stringPtr]],
					pasori_close: ["void", [intPtr]],
					pasori_init: ["int", [intPtr]],
					felica_polling: [intPtr, [intPtr, "uint16", "uint8", "uint8"]],
					felica_free: ["void", [intPtr]],
					felica_getidm: ["void", [intPtr, "char*"]],
					felica_getpmm: ["void", [intPtr, "char*"]],
					felica_read_without_encryption02: ["int", [intPtr, "int", "int", "uint8", "uint8"]]
				});
			} catch (e) {
				throw "ライブラリの読み込みにエラーが発生しています。";
			}

		}

		if (FelicaAccessar.pasoriPtr === null) {
			let pasoriPtr = FelicaAccessar.felicalib.pasori_open(null);

			if (pasoriPtr.length === 0) {
				throw "felicalib.dll ファイルを開くことが出来ません。";
			}
			if (FelicaAccessar.felicalib.pasori_init(pasoriPtr) !== 0) {
				throw "PaSoRi に接続できません。";
			}

			FelicaAccessar.pasoriPtr = pasoriPtr;
		}
	},
	/**
	 * PaSoRiとの接続を終了する。
	 * もしもそもそも接続をしていない（ポインタを変数に持っていない）ときはとりあえず何もしない。
	 */
	Close: () => {
		if (FelicaAccessar.pasoriPtr !== null) {
			FelicaAccessar.felicalib.pasori_close(FelicaAccessar.pasoriPtr);
			FelicaAccessar.pasoriPtr = null;
		}
	},
	/**
	 * タッチされているカードの情報を取得する。
	 * カード情報はFelicaAccessar.felicapに入っている。また、取得成功\失敗情報はbool値で返している。
	 * @return {boolean}
	 */
	Polling: () => {
		FelicaAccessar.felicalib.felica_free(FelicaAccessar.felicap);
		FelicaAccessar.felicap = FelicaAccessar.felicalib.felica_polling(FelicaAccessar.pasoriPtr, "0xffff", 0, 0);
		if (FelicaAccessar.felicap.length === 0) {
			return false;
		} else {
			return true;
		}
	},
	/**
	 * Pollingで取得したカード情報からidmデータを取得する。
	 * 正確に言うと、現在 FelicaAccsessar.felicap に登録されているBufferをFelicaカードだと仮定して、idm情報を取得する。
	 * もしも FelicaAccesser.felicap がFelicaカードのポインタではない時、catchできないエラーが発生してアプリケーションが止まる。注意。
	 * @return {string}
	 */
	IDm: () => {
		if (FelicaAccessar.felicap.length === 0) {
			throw "タッチされていません";
		}

		//Pollingで取得したカード情報からidm情報を取得する。
		//この時点で取得できるのは10進数で表記された8個の整数。
		let byte = new Uint8Array(8);
		FelicaAccessar.felicalib.felica_getidm(FelicaAccessar.felicap, byte);

		//16進数表記の文字列に変更する。
		let id = "";
		for (let i = 0; i < byte.length; i++) {
			let tmp = byte[i].toString(16);

			id += ("00" + tmp).slice(-2);
		}

		return id;
	},
	/**
	 * Polling()とIDm()の関数をいい感じに利用できるようにした部分。
	 * これを実行すると、1秒に1回カード情報を取得する処理を実行する。
	 * カード情報を受け取った時にこの取得処理自体をストップし、コールバック関数を実行している。
	 * システム内でカード情報を利用した処理の終了後、再びこの関数を実行すること。
	 *
	 * @param {function} callback 詳細は renderer.js の onTouchFelica() 参照
	 * 第一引数に現在の状態。第二引数にidmまたはエラー文字列。
	 * 0=正常（タッチ発生） 1=エラー発生 2=エラー続行 3=エラー解消（正常続行）
	 * 0かつ3のときは0の処理を優先する。
	 * 2の時は普通にpolling（再接続）をするためコールバックしない。
	 */
	loopPollingPaSoRi: (callback) => {
		let Polling = FelicaAccessar.Polling;
		let IDm = FelicaAccessar.IDm;

		let timer = setInterval(() => {
			try {
				FelicaAccessar.Felica();

				let isReading = Polling();

				//タッチ中フラグが立っている＆タッチしているとき、何もしない
				if (FelicaAccessar.isTouching && isReading) {
					//no-op
				}
				//タッチ中フラグが立っていない＆タッチしている時、タッチ処理を行う
				else if (FelicaAccessar.isTouching === false && isReading) {
					let idm = IDm();
					clearInterval(timer);
					FelicaAccessar.isTouching = true;

					if (callback !== undefined) {
						callback(0, idm);
					}
				}
				//タッチ中フラグが立っている＆タッチしていないとき、タッチ中フラグを折る
				else if (FelicaAccessar.isTouching && isReading === false) {
					FelicaAccessar.isTouching = false;
				} else {
					//no-op
				}

				FelicaAccessar.Close();

				//問題なく動作完了したためerrorFlagが立っていれば折る。
				if (FelicaAccessar.errorFlag === true) {
					FelicaAccessar.errorFlag = false;
					clearInterval(timer);
					callback(3, "PaSoRiを確認しました。");
				}
			} catch (e) {
				if (FelicaAccessar.errorFlag === false) {
					clearInterval(timer);
					FelicaAccessar.errorFlag = true;
					callback(1, e);
				}
			}
		}, 1000);
	},
};
module.exports = FelicaAccessar;
```

またindex.jsを以下のように更新します。

```javascript:index.js
const electron = require('electron');
const {app, BrowserWindow, dialog} = electron;
const path = require('path');
const url = require('url');
const FelicaAccesser = require('./FelicaAccesser');

let mainWindow;

//アプリケーション（基幹部）の起動が終了後動作する。
app.on('ready', () => {
	createWindow();
	FelicaAccesser.Felica();
	FelicaAccesser.loopPollingPaSoRi(onTouchFelica);
});

function createWindow() {
	// Create the browser window.
	mainWindow = new BrowserWindow({
		width: 400,
		height: 300,
		useContentSize: true,
		title: "test",

	});

	mainWindow.loadURL(url.format({
		pathname: path.join(__dirname, 'index.html'),
		protocol: 'file:',
		slashes: true
	}));

	mainWindow.webContents.openDevTools();

	//窓全部閉じたときnullにする
	mainWindow.on('closed', () => {
		mainWindow = null
	});
}

function onTouchFelica(status, idm) {
	//タッチまたはFelicaの接続が切れた時処理時実行
	if (status === 0) {
		dialog.showMessageBox({
			type: "info",
			title: "touch card",
			message: idm
		});
    } else if (status === 3) {
		dialog.showMessageBox({
			type: "info",
			title: "restart PaSoRi",
			message: "",
		});
	} else if (status === 1) {
		dialog.showMessageBox({
			type: "error",
			title: "error no PaSoRi",
			message: "",
		});
	} else {
		//何もしない
	}

	//処理終了。タッチイベントを再開する。
	FelicaAccesser.loopPollingPaSoRi(onTouchFelica);
}
```

## 実行

それでは`npm start`で実行し、問題なくアプリケーションが動作することを確認してください。
Felicaをかざしたらidm情報がダイアログで表示されれば問題ありません。
また、一応PaSoRi側に問題が発生した時もいろいろ出るようになってます。

# asarでまとめてパッケージングする

## asarとは？
以下のページが非常に詳しいのでおすすめします。
[asarを使ってみる](https://qiita.com/niusounds/items/2739fdbc26df03421bfe)

それではasarを入手…してもいいのですが、今回はもう少し簡単な手段でやります。

## electron-packagerをインストールする
[electron-userland/electron-packager(Github)](https://github.com/electron-userland/electron-packager)とは、Electronアプリケーションをいい感じにパッケージングするための仕組みです。
以下のようにインストールを行ってください。

```
your-project> npm install electron-packager --save-dev
```

細かい説明についてはgithubの説明を直接見てもらうとして、このパッケージャーはオプションでasar圧縮を指定することができます。
以下のようにして圧縮することができます。

```
your-project> electron-packager . test ––platform=darwin,win32 ––arch=x64 ––version=0.0.1 --asar
```

アイコンなどの設定をしたかったら、オプションを増やしてください。

パッケージング後、プロジェクトディレクトリ内に`test-win32-is32`のような形式でディレクトリが作られているかと思われるので、それを開き、中にある`test.exe`を実行してみてください。**エラーが出ます**

原因として考えられるのは「.dllファイルは直接パソコンに受け渡す必要があるから」らしいのですが……正確なところは知識がないので不明です。
しかしtest.exeのあるディレクトリに直接`felicalib.dll`ファイルを放り込むと問題なく動作するので、これでとりあえず良いでしょう。

先ほど開いた`test.exe`を一度閉じて再び開きなおすと、エラー無く起動します。

# おわり

これでデスクトップアプリケーションが完成しました。あとはソースコードを弄るなりもう少し見た目を良くするなり、きちんとパッケージングするなり頑張ってください。

おつかれさまでした。

# 気になる点

* 開発中のディレクトリと、完成後のパッケージングされたディレクトリ、二重でfelicalib.dllが入ってしまっている。
* すべてプロジェクトディレクトリ内で完結させてしまったので、不必要なファイルが大量に含まれている。
* test.exeのあるディレクトリ内に大量の.dllファイルが入ってしまっていてあまりきれいな状態とは言えない。