チュートリアル

Monacaのサンプルプロジェクト

以下のこちらのチュートリアルは JINS MEME Monaca Sampleの公開プロジェクトからご自身の環境にインポートを行い、動作を確認していただくことが可能です(要アプリid/secretの取得)。

また、首振り・視線移動によるトリガ検出のデモサンプル も公開となっておりますので併せてご利用ください。

アプリの動作検証

アプリの動作検証をするには、Monacaデバッガーを利用するか、アプリをビルドして端末にインストールします。

開発中はMonacaデバッガーを利用すると、毎回コンパイルの必要がなく便利です。ただしApp Store / Google Play版のMonacaデバッガーにはJINS MEME SDK for Monacaが含まれていないので、カスタムビルド版のMonacaデバッガーを作成して下さい(2019/03現在、カスタムビルド版デバッガーの作成には有償プランの契約が必要です)。

カスタムビルド版Monacaデバッガーの作成方法は以下のドキュメントを参考にして下さい。

アプリの実装

JINS MEME SDK for Monacaは、以下の手順で実装します。

  1. アプリID、アプリSecretの設定
  2. 近くにあるJINS MEMEデバイスをスキャン
  3. アプリとJINS MEMEデバイスの接続(ペアリング)
  4. データ送信の開始
  5. データ送信の終了、再接続

ここでは、まばたきと体の傾きを可視化するサンプルアプリを題材として、JINS MEME SDK for Monacaによるアプリ開発方法を紹介します。

アプリの完成イメージは以下のようになります。

  • まばたき未検出時

まばたき未検出時

  • まばたき検出時

まばたき検出時

  • 体の傾き

体の傾き

  • 画面構成

画面はOnsen UIを利用して実装しています。

<body>
  <ons-page>
    <!-- ツールバー -->
    <ons-toolbar>
      <div class="center">JINS MEME</div>
    </ons-toolbar>
    
    <!-- タブバー -->
    <ons-tabbar position="auto" id="tabbar">
      <ons-tab label="Eye" page="tab1.html" icon="eye" active>
      </ons-tab>
      <ons-tab label="Body" page="tab2.html" icon="male">
      </ons-tab>
    </ons-tabbar>
  </ons-page>

  <!-- Eyeタブ -->
  <ons-template id="tab1.html">
    <ons-page id="first-page">
      <div style="text-align: center;">
        <p>まばたきを検出します。</p>
        <ons-icon id="icon-eye" icon="eye" size="200px"></ons-icon>
      </div>
    </ons-page>
  </ons-template>

  <!-- Bodyタブ -->
  <ons-template id="tab2.html">
    <ons-page id="second-page">
      <div style="text-align: center;">
        <p>体の傾きを検出します。</p>
        <ons-icon id="icon-body" icon="male" size="200px"></ons-icon>
      </div>
    </ons-page>
  </ons-template>
  
  <!-- デバイス選択ダイアログ -->
  <ons-dialog id="selectDeviceDialog" cancelable>
    <ons-list id="deviceList">    
    </ons-list>
  </ons-dialog>
  
  <!-- モーダルウィンドウ -->
  <ons-modal id="modal">
    <p>接続中...</p>
    <ons-icon icon="spinner" size="28px" spin></ons-icon>
  </ons-modal> 
</body>

1. アプリID、アプリSecretの設定

まずはアプリの起動時に発生するdevicereadyイベント内で、アプリIDとアプリSecretを設定します。

// 起動時のイベント
document.addEventListener('deviceready', () => {
    // アプリの初期化処理
    cordova.plugins.JinsMemePlugin.setAppClientID(
        '', //client id
        '', //secret
        () => {
            restartScan();
        },
        () => {
            console.log('Error: setAppClientID');
        }
    );
});

cordova.plugins.JinsMemePlugin.setAppClientID() は、アプリIDとアプリSecretを元にアプリの認証を行います。 第三引数には認証完了後に実行する処理、第四引数には認証失敗時に実行する処理を指定します。

認証に成功した時に呼ばれるrestartScan()は再接続時にも呼ばれるため、既にJINS MEMEと接続・データ送信中であることも考慮し、データ送信停止→切断→スキャン停止→スキャン開始を実施します。こちらの処理では初回起動時にはエラーになることもありますが、上記のように再接続時にも同じ関数をコールできるようにするためこれらの処理が必要です。内部で呼ばれている関数に関しては後述します。

// デバイスのスキャン再開
const restartScan = () => {
    stopDataReport();//他アプリとの整合性のため
    disconnect();//すでにアプリが接続済みの場合に切断を行う
    stopScan(startScan);//複数回restartScanが呼ばれた時(scan中の時)にscanを同時に走らないようにstopをかけてからscan
}

2. 近くにあるJINS MEMEデバイスをスキャン

アプリの認証に成功したら、 cordova.plugins.JinsMemePlugin.startScan() でスキャンを開始します。 第一引数にデバイスが見つかった時の処理、第二引数にスキャンに失敗したときの処理を指定します。

ただし、スキャンを既に実行している場合は、一度スキャンの停止処理を行ってからスキャンを再開しないとエラーが発生します。 そこで、まず cordova.plugins.JinsMemePlugin.stopScan() を実行してからスキャンを開始しています。

// デバイスのスキャン停止
const stopScan = successCallback => {
    cordova.plugins.JinsMemePlugin.stopScan(() => {
        if(successCallback) successCallback();
    }, () => {
        console.log('Error: stopScan');
    });
}

// デバイスのスキャン開始
const startScan = () => {
    // デバイス選択ダイアログを表示
    const deviceList = document.getElementById('deviceList');
    deviceList.innerHTML = '<ons-list-header>デバイスを選択</ons-list-header>';
    document.getElementById('selectDeviceDialog').show();
    
    cordova.plugins.JinsMemePlugin.startScan(device => {
        // ダイアログにデバイスを追加
        deviceList.innerHTML += "<ons-list-item tappable onclick=\"connect('" + device + "')\">" + device + "</ons-list-item>";
    }, () => {
        console.log('Error: startScan');
    });
}

今回はOnsen UIで以下のようなデバイス選択ダイアログをあらかじめ用意しておき、検出されたデバイス情報を動的にダイアログ内に追加しています。

<!-- デバイス選択ダイアログ -->
<ons-dialog id="selectDeviceDialog" cancelable>
  <ons-list id="deviceList">
  </ons-list>
</ons-dialog>

JINS MEME側は、フレームにある電源ボタンを2秒ほど長押しして接続待ちの状態にします。 電源ボタンの上のLEDが青く点滅していれば接続待ちになっています。

デバイスが検出されると、以下のようにデバイス選択ダイアログが表示されます。

デバイスの選択

3. アプリとJINS MEMEデバイスの接続(ペアリング)

デバイス選択ダイアログの中からデバイスが選択されたら、cordova.plugins.JinsMemePlugin.connect() でデバイスとの接続を実行します。 第一引数に接続完了時の処理、第二引数に将来デバイスが切断されたときの処理(デバイスの電源が切れた場合やアプリとデバイスの距離が遠く離れた場合など)、第三引数に接続失敗時の処理を指定します。

// アプリとデバイスの接続
const connect = device => {
    // スキャン停止
    stopScan();
    // ダイアログを閉じてモーダルを表示
    document.getElementById('selectDeviceDialog').hide();
    document.getElementById('modal').show();
            
    // 選択されたデバイスに接続
    cordova.plugins.JinsMemePlugin.connect(device, () => {
        //端末依存で不安定な場合があるのでウェイトをかける処理
        setTimeout(stopDataReport, 500);
        setTimeout(startDataReport, 1000);
    }, () => {
        console.log('Disconnect');
    }, () => {
        console.log('Error: connect');
        document.getElementById('modal').hide();
    });
}

4. データ送信の開始

デバイスとの接続が完了したら、cordova.plugins.JinsMemePlugin.startDataReport() でデータ送信を開始します。 第一引数にはデータ送信データを取得したタイミングで実行される処理を指定します。この処理はデータ送信が停止されるまでの間、定期的(数十ミリ秒間隔)に実行されます。 第二引数には、データ送信の開始に失敗したときの処理を指定します。

// データ送信開始
    const startDataReport = () => {
        document.getElementById('modal').hide();
        cordova.plugins.JinsMemePlugin.startDataReport(data => {
            draw(data);
        }, () => {
            console.log('Error: startDataReport');
        });
    }

// データ送信結果を描画する
const draw = data => {
    let tabIndex = document.getElementById('tabbar').getActiveTabIndex();
    if (tabIndex === 0) {
        // まばたきされたらアイコンを変更する
        if(data.blinkSpeed > 0 || data.blinkStrength > 0) {
            document.getElementById('icon-eye').setAttribute('icon', 'eye-slash');
        } else {
            document.getElementById('icon-eye').setAttribute('icon', 'eye');
        }
    } else if(tabIndex === 1) {
        // 姿勢角Rollに合わせてアイコンを傾ける
        const deg = data.roll * -1;
        document.getElementById('icon-body').style['transform'] = `rotate(${deg}deg)`;
    }
}

体の傾きは、アイコン画像をアニメーションつきで傾けて表現しています。 以下のようにCSSを指定して実現しています。

#icon-body {
    transition-property: transform;
    transition-duration: 0.1s;
}

5. データ送信の終了、再接続

切断とデータ送信停止はこのプログラムでは単独で呼ばれることはなく、SDKをwrapしただけの形になります。再接続アイコンをタップした時に呼ばれるreconnectは、切断に成功した場合に、restartScan()を実施します。

// アプリとデバイスの切断
const disconnect = () => {
    cordova.plugins.JinsMemePlugin.disconnect(function() {}, function() {
        console.log('Error: disconnect');
    });
}

// データ送信停止
const stopDataReport = () => {
    cordova.plugins.JinsMemePlugin.stopDataReport(
      () => {

      },
      () => {
        console.log('Error: stopDataReport');
    });
}

// 再接続
const reconnect = () => {
  ons.notification.confirm("再接続しますか?")
  .then(result => {
    if(result)
      cordova.plugins.JinsMemePlugin.disconnect(restartScan, error => {
        console.log('Error:reconnect ' + error.code + ' : ' + error.message);
      });
  })
}

最終版のindex.html

すべてを結合し、ヘッダーに必要なJS/CSSの読み込み処理を加えたものが以下になります。お疲れ様でした。

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">
    <script src="components/loader.js"></script>
    <script src="lib/onsenui/js/onsenui.min.js"></script>
  
    <link rel="stylesheet" href="components/loader.css">
    <link rel="stylesheet" href="lib/onsenui/css/onsenui.css">
    <link rel="stylesheet" href="lib/onsenui/css/onsen-css-components.css">
    <link rel="stylesheet" href="css/style.css">

    <script>
    // 起動時のイベント
    document.addEventListener('deviceready', () => {
        // アプリの初期化処理
        cordova.plugins.JinsMemePlugin.setAppClientID(
            '223460896375733', //client id
            'jhg662204x7e2r541r5egso1fwhsrltz', //secret
            () => {
                restartScan();
            },
            () => {
                console.log('Error: setAppClientID');
            }
        );
    });

    // デバイスのスキャン再開
    const restartScan = () => {
      stopDataReport();//他アプリとの整合性のため
      disconnect();//すでにアプリが接続済みの場合に切断を行う
      stopScan(startScan);//複数回restartScanが呼ばれた時(scan中の時)にscanを同時に走らないようにstopをかけてからscan
    }

    // デバイスのスキャン停止
    const stopScan = successCallback => {
        cordova.plugins.JinsMemePlugin.stopScan(() => {
            if(successCallback) successCallback();
        }, () => {
            console.log('Error: stopScan');
        });
    }

    // デバイスのスキャン開始
    const startScan = () => {
        // デバイス選択ダイアログを表示
        const deviceList = document.getElementById('deviceList');
        deviceList.innerHTML = '<ons-list-header>デバイスを選択</ons-list-header>';
        document.getElementById('selectDeviceDialog').show();

        cordova.plugins.JinsMemePlugin.startScan(device => {
            // ダイアログにデバイスを追加
            deviceList.innerHTML += "<ons-list-item tappable onclick=\"connect('" + device + "')\">" + device + "</ons-list-item>";
        }, () => {
            console.log('Error: startScan');
        });
    }

    // アプリとデバイスの接続
    const connect = device => {
        // スキャン停止
        stopScan();
        // ダイアログを閉じてモーダルを表示
        document.getElementById('selectDeviceDialog').hide();
        document.getElementById('modal').show();

        // 選択されたデバイスに接続
        cordova.plugins.JinsMemePlugin.connect(device, () => {
            //端末依存で不安定な場合があるのでウェイトをかける処理
            setTimeout(stopDataReport, 500); //SDK整合性担保のため
            setTimeout(startDataReport, 1000);
        }, () => {
            console.log('Disconnect');
        }, () => {
            console.log('Error: connect');
            document.getElementById('modal').hide();
        });
    }

    // アプリとデバイスの切断
    const disconnect = () => {
        cordova.plugins.JinsMemePlugin.disconnect(function() {}, function() {
            console.log('Error: disconnect');
        });
    }
    // データ送信開始
    const startDataReport = () => {
        document.getElementById('modal').hide();
        cordova.plugins.JinsMemePlugin.startDataReport(data => {
            draw(data);
        }, () => {
            console.log('Error: startDataReport');
        });
    }
    
    // データ送信停止
    const stopDataReport = () => {
        cordova.plugins.JinsMemePlugin.stopDataReport(
          () => {

          },
          () => {
            console.log('Error: stopDataReport');
        });
    }

    // データ送信結果を描画する
    const draw = data => {
        let tabIndex = document.getElementById('tabbar').getActiveTabIndex();
        if (tabIndex === 0) {
            // まばたきされたらアイコンを変更する
            if(data.blinkSpeed > 0 || data.blinkStrength > 0) {
                document.getElementById('icon-eye').setAttribute('icon', 'eye-slash');
            } else {
                document.getElementById('icon-eye').setAttribute('icon', 'eye');
            }
        } else if(tabIndex === 1) {
            // 姿勢角Rollに合わせてアイコンを傾ける
            const deg = data.roll * -1;
            document.getElementById('icon-body').style['transform'] = `rotate(${deg}deg)`;
        }
    }

    // 再接続
    const reconnect = () => {
      ons.notification.confirm("再接続しますか?")
      .then(result => {
        if(result)
          cordova.plugins.JinsMemePlugin.disconnect(restartScan, error => {
            console.log('Error:reconnect ' + error.code + ' : ' + error.message);
          });
      })
    }

    </script>
  </head>
  <body>
    <ons-page>
      <!-- ツールバー -->
      <ons-toolbar>
        <div class="center">JINS MEME</div>
        <div class="right">
          <ons-toolbar-button>
              <ons-icon icon="plug" size="24px" onclick="reconnect()"></ons-icon>
          </ons-toolbar-button>
        </div>
      </ons-toolbar>

      <!-- タブバー -->
      <ons-tabbar position="auto" id="tabbar">
        <ons-tab label="Eye" page="tab1.html" icon="eye" active>
        </ons-tab>
        <ons-tab label="Body" page="tab2.html" icon="male">
        </ons-tab>
      </ons-tabbar>
    </ons-page>

    <!-- Eyeタブ -->
    <ons-template id="tab1.html">
      <ons-page id="first-page">
        <div style="text-align: center;">
          <p>まばたきを検出します。</p>
          <ons-icon id="icon-eye" icon="eye" size="200px"></ons-icon>
        </div>
      </ons-page>
    </ons-template>

    <!-- Bodyタブ -->
    <ons-template id="tab2.html">
      <ons-page id="second-page">
        <div style="text-align: center;">
          <p>体の傾きを検出します。</p>
          <ons-icon id="icon-body" icon="male" size="200px"></ons-icon>
        </div>
      </ons-page>
    </ons-template>

    <!-- デバイス選択ダイアログ -->
    <ons-dialog id="selectDeviceDialog" cancelable>
      <ons-list id="deviceList">
      </ons-list>
    </ons-dialog>

    <!-- モーダルウィンドウ -->
    <ons-modal id="modal">
      <p>接続中...</p>
      <ons-icon icon="spinner" size="28px" spin></ons-icon>
    </ons-modal>
  </body>
</html>