はじめてのElectronとESP-WROOM-02でカミワザ!!


やりたいこと

最近うちの子どもが「カミワザワンダ」という、妖怪ウォッチ的なパクリアニメにハマっております。このアニメ、妖怪ウォッチの妖怪メダルのように、主人公がカミワザプロカというカードを集めるのですが、もちろんカミワザプロカも現実世界でもおもちゃとして売られております。このカード、RFIDが埋め込まれているんですね(妖怪メダルはQRコード?)。
そこで、子どもの心を鷲掴みにするべく、カミワザプロカのRFIDを読み取って、対応するキャラクターをMacの画面にかっこよく表示するアプリを作りたいと思います。このアプリ、「カミワザ図鑑」と勝手に名付けましょう。せっかくなので、ずっと気になっていたElectronでデスクトップアプリとして作りたいと思います。イメージ的にはこんな感じです。

用意するもの

まず、RFIDを読み取るために簡単な電子工作が必要です。RFIDリーダーとElectronアプリとの間はWiFi経由で通信しますので、WiFiが簡単に使えるESP-WROOM-02を選びました。
- ESP-WROOM-02開発ボード
- RFID-RC522
- ブレッドボード、ジャンパワイヤー、LED

作成手順

RFIDリーダ組み立て

まず、ESP-WROOM-02とRFID-RC522で自作RFIDリーダを組立てます。以下の参考URLを真似して、ブレッドボード上にジャンパワイヤを差していくだけです。(RC522のピンだけ半田付け必要)

ESP-WROOM-02のプログラム

次に、RFIDリーダの動作をプログラミングします。RFIDリーダーにRFIDをかざすと、そのUUIDをElectronアプリにHTTPで送信するようにします。
ESP-WROOM-02の開発はArduinoIDEで行えます。ArduinoIDEをダウンロード&インストールしたら以下の参考URLに従ってセットアップしましょう。

RFIDを読み取る処理は先ほどの「ESP-WROOM-02とRFID-RC522で非接触Lチカ」のコードをそのまま流用し、RFIDのUIDを取得する処理は「Arduinoで複数のNFCタグを区別する」から以下のコードを流用させていただきました。

read.c
  String strBuf[mfrc522.uid.size];
    for (byte i = 0; i < mfrc522.uid.size; i++) {
      strBuf[i] =  String(mfrc522.uid.uidByte[i], HEX);  // (E)using a constant integer
      if(strBuf[i].length() == 1){  // 1桁の場合は先頭に0を追加
        strBuf[i] = "0" + strBuf[i];
      }
    }

流用しかありませんが、一応ソースをGithubにあげました。

アプリのプログラム

Node.jsインストール

ようやく本題。アプリはElectronで作ります。Electronアプリ開発のために、まずはNode.jsのインストール。Macであればbrewで簡単にインストールできます。

$ brew install node

Electronインストール

続いてElectronのインストールです。npmで簡単です。

$ npm -g install electron-prebuilt

その他ライブラリインストール

これで最低限の環境が整いました。今回、ElectronアプリはRFIDリーダからのHTTPリクエストを受け付けるので、WebアプリケーションフレームワークであるExpressもインストールします。HTTPリクエストを受け付けるだけなのに少々大げさな気もしますが、便利なので使います。
また、表示部のバインディングにVue.jsを使ってみたいと思いますのでこれもインストールします。

$ npm install --save express vue

ExpressによるHTTPリクエスト受け付け

Electron内でExpressを立ち上げるには、Expressをロードしてlistenメソッドを呼びだします。
空のWindowだけ立ち上げて、Expressでリクエストを受け付けるサンプルは以下になります。

main.js
/**
 * メインプロセス
 */   
const electron = require('electron');
const Express = require("express");
const express = Express();

let app = electron.app;
let BrowserWindow = electron.BrowserWindow;

app.on('ready', function() {
  mainWindow = new BrowserWindow({});
  /**
   * 画面にHTMLファイルを表示したい場合はloadURLで読み込む
   */
  // mainWindow.loadURL('file://' + __dirname + '/index.html');

  mainWindow.webContents.on('did-finish-load', () => { 
      //load後に実施したい処理はここに書く
  }); 

  mainWindow.on('closed', function() {
    mainWindow = null;
  });
});

/**
 * Expressのセットアップ
 */
express.listen(3000, "localhost");
express.get('/:id', function(req, res, next) {
    /**
     * "GET /123"のリクエストを受信すると
     *  req.params.idに123が入る
     */
    console.log("Request Param is " + req.params.id);
    res.sendStatus(200);
}); 

起動は、$ electron main.jsです。ウィンドウが立ち上がったら、ブラウザからhttp://local:3000/Helloとか入力して、200応答が返ってきたら成功です。

プロセス間通信

Electronはメインプロセスと、描画を担当するレンダラープロセスに分かれているようです。上のサンプルでは、main.jsがメインプロセスで、loadURLでロードされたHTMLファイルに記述されたJavaScriptがレンダラープロセスで動作するイメージです。Expressはメインプロセスで動いているので、Epxressが受け取ったHTTPリクエストの情報を画面に反映するには、レンダラープロセスへのプロセス間通信(IPC)が必要になります。
IPCは最近仕様が変わったようで、動かないサンプルの記事に苦しめられました。Electronバージョン1.4.3では以下のやりかたのようです。

メインプロセスからレンダラープロセスに通信

main.js
mainWindow = new BrowserWindow({});
// 'keyword'で待ち受けるrenderer側に第2引数(Hello)を送信
mainWindow.webContents.send('keyword', 'Hello');
renderer.js
const ipcRenderer = require( 'electron' ).ipcRenderer;
// 'keyword'宛のメッセージを待ち受ける
ipcRenderer.on('keyword', function(ev, msg) {
    // メッセージを受け取ったあとの処理
});

音を鳴らす

IPCでメインプロセスからRFIDの情報を受け取ったら、レンダラープロセス側ではVue.jsによって画面表示を切り替えるとともに、RFIDに対応するカッコいいサウンドを鳴らします。音を鳴らすのにはHTML5のaudioタグを使いました。

index.html
<div id="app" class="audio">
    <p>{{sound}}</p>
    <audio id="audio" controls>
        <source :src="sound"/>
    </audio>    
</div> 

vue.jsで{{sound}}の値を切り替えるだけでは再生しないので、コントローラからload()とplay()が必要です。load()を忘れてしばらくはまりました。

renderer.js
app.sound="./audio/sound.mp3"; //Viewの書き換え
audio.load(); // 再読み込み ★ここ重要
audio.play(); // 音を鳴らす

デバッグ

レンダラープロセスが思う通りに動かない時に、デバッグのために差し込んだconsole.logがどこにも表示されず困りました。
結論としては、メインプロセス側でopenDevTools()を呼んでおくと開発ツールっぽいのが表示されて、コンソールログも無事に見れるようになりました。

main.js
let BrowserWindow = require('electron').BrowserWindow;
mainWindow = new BrowserWindow({width: 800, height: 680});
mainWindow.webContents.openDevTools(); //←ここ

その他ハマったところ

Electron上でHTMLのinputフォームを作ったのですが、なにも入力できない状態になりました。どうやら、tmuxからElectronを起動しているのが原因だったようです(#773)。tmuxを経由しないでElectronを起動したところ、無事に入力できるようになりました。

できあがり

pic.twitter.com/mr788cc25V

— saga (@vimyum) February 25, 2017

ソースコードはGithubにあげてます。画像や音声ファイル、カード説明文は著作権に引っかかると思うので、削除してます。