(改題:3/19)
旧タイトル「※公式対応済み※【初心者向け】JINS MEME SDK for Monacaでバグだらけのチュートリアルをデバッグして動かすまで」
理由:
- 表現が誇大(「バグ」というよりケアレスミス、「だらけ」というほど多くない)
- 既に修正済み
- この記事自体修正が多く、人のこと言えないので…
(コメントを受け追記・修正:3/19)
公式のチュートリアルが修正済みです。(対応がお早い!)
言うまでもなく、この記事よりも公式をご覧頂いたほうが確かな情報が得られます。
以下本記事は私の備忘録としての意味しか持ちません。
(追記終わり)
(コメントを受け追記 3/18)
JINS MEMEの中の方よりコメントいただきました。修正していただけるとのことなので、まずはそちらをご覧ください。
チュートリアル - JINS MEME DEVELOPER DOCUMENTATION
本記事は3/18閲覧時点の情報に基づいております。最新の情報と異なる場合がございますので、予めお含みおきください。なお、本記事中の誤りはすべて私個人に属します。
(追記終わり)
はじめに
この記事の対象
- スマホアプリ開発初心者
- Monaca?なにそれ美味しいの?
- 手っ取り早くJINS MEMEのアプリ開発がしたい!
- でも公式ドキュメントやQiita記事読んでもよく分からん!
要するに私のことです。
経緯
- 今週JINS MEMEの存在を知り、早速購入
- 最もとっつきやすそうなMonacaでチュートリアルのコードをコピペしてみるも、うまく動かず
- Monacaの使い方が悪いのかコードが悪いのかデバイス間の接続が悪いのかといった問題の切り分けに苦労したので、初心者向けとして記録しておきます
書かないこと
- JINS MEMEとは
- monacaとは
- iOSアプリのビルド
初めの一歩:JINS MEME DEVELOPERSとmonacaのアカウント登録
この記事を読んでいるあなたは恐らく、「JINS MEME SDKへようこそ - JINS MEME DEVELOPER DOCUMENTATION」を読んで途方に暮れているところでしょう。私がそうでした。
ドキュメントを行ったり来たりしながら結局ほしい記述がなかったりしたので、差し当たって必要な部分のみ紹介・補足していきます。
JINS MEME DEVELOPERSへの登録
「開発を始める - JINS MEME DEVELOPER DOCUMENTATION」の通りなんですが、いきなり必須項目の「プライバシーポリシーURL」の意味がわからず戸惑います。
monaca版では書かれていませんが、Android版なんかではこんな記載があります。
※ここで入力する情報は、作成予定のアプリと必ずしも一致させる必要はありません。
ええんかい。という訳で実在しないURLでも入力してさくっと登録をすませましょう。実際に開発したアプリをリリースする際にどうすればよいのかについては、またそのとき検討することとします。
Monacaのアカウント作成
続いてMonacaでアカウントを作りましょう。またAndroid版との比較ですが、そっちではAndroid Studioのダウンロードから丁寧に説明されているのに、Monaca版ではMonaca上での操作に関する記述は僅かしかありません。そのおかげで自ら試行錯誤して理解を深めることができます。
といってもアカウント作成自体はいたって普通のウェブサービスと同様なので迷うことはありません。一つ重要な点は、Proプラン(以上)を選ぶことです。2週間のトライアル期間中はどのプランも選べますし変更も自由ですが、Freeプランで登録してそのまま開発をしようとするとすぐに困ることになります。(後述)
カスタムビルド版 Monaca デバッガーのビルド
チュートリアルでは最後にさらっと触れているだけですが、ここが最も重要です。
これがちゃんとできてないと、デバッグができないばかりでなく「Monacaの使い方か、コーディングか、どこが悪いのか分からない」状態になり不要な混乱を惹起すること請け合いなので、最初にしっかりやっておきましょう。
公式のデバッガーではなぜだめなのか?
既に答えは書かれているのですが、
App Store / Google Play版のMonacaデバッガーにはJINS MEME SDK for Monacaが含まれていないため、JINS MEME連携アプリの動作検証ができません。
実際にどうだめなのか試してみましょう。
チュートリアルのコードをコピペして公式のデバッガーで動かしてみます。すると以下のようなエラーが出ます。
Uncaught TypeError: Cannot read property 'JinsMemePlugin' of undefined
まぁ予想通りですね。
カスタムビルドする
ということでデバッガーをカスタムビルドする訳ですが、ここでいくつか落とし穴があります。順に見ていきましょう。
JINS MEME SDK Pluginの有効化
適当にプロジェクトを作成したあと、「開発を始める - JINS MEME DEVELOPER DOCUMENTATION」に従いPluginを有効化します。
ただしMonaca側で若干UIが変更されおり、現在ではプロジェクトのIDEで「設定」→「Cordovaプラグインの管理」です。
以下「落とし穴①」と「落とし穴②」の内容には誤りが含まれています。先にコメント欄をご覧ください。(追記:3/19)
落とし穴①:Pluginのインポート
後はチュートリアルからもリンクされてる「Android 向けの Monaca デバッガー | Monaca Docs」通りにビルドすればいいかと思ってしまうと最初の落とし穴です。
実際にやってみましょう。するとこんなエラーが出ます。
アプリのビルドに失敗しました。次の項目を確認してください。
Failed to fetch plugin. There is a connection problems, or plugin specification is incorrect.
エラーを修正した後で、再度ビルドを行ってください。
ビルドログではこんな感じ(抜粋)
Discovered plugin "monaca-plugin-jins-meme" in config.xml. Adding it to the project
Failed to restore plugin "monaca-plugin-jins-meme" from config.xml. You might need to try adding it again.
Error: Failed to fetch plugin monaca-plugin-jins-meme@1.3.0 via registry.
Probably this is either a connection problem, or plugin spec is incorrect.
なにが悪いのかというと、JINS MEME SDK Pluginは有効化するだけではなくインポートする必要があるのです。(参照)
カスタムビルド版デバッガーをビルドする前に、ユーザー Cordova プラグインまたは外部の Cordova プラグインを必ず 「 インポート 」 してください。
なぜこんな大事なことがこっちには記載されてないのだ…。釈然としない気持ちを抱えつつ、当該プラグインをインポートしましょう。手順は「設定」→「Cordovaプラグインの管理」にて、「Cordovaプラグインのインポート」→「パッケージ名 / URL」で「monaca-plugin-jins-meme」を入力し「OK」。
落とし穴②:フリープランの制約
ところがここで、Freeプランで登録した方には2つ目の落とし穴です。上記手順で操作するとYour plan dose not support Cordova Pluginsのエラーが。
というのも、 Freeプランでは利用できるCordovaPluginはコアプラグインのみに限られているのです。
なお、**デバッガーではなくJINS MEMEアプリ自体のビルドにあたっては、インポートの手順は不要です。**従って、トライアル期間中にproプランでJINS MEME開発用のカスタムデバッガをビルドして端末にインストールし、トライアル期間が終わった後はフリープランでJINS MEMEアプリの開発を続けることができますのでご安心ください。
チュートリアルのコードをデバッグする
カスタムビルドデバッガーのインストールまで完了したでしょうか。さぁ、これでようやく開発環境が整いました。
早速チュートリアルをデバッガで実行してみましょう。
……どうでしょうか。動きませんね。
(追記始まり 3/18)
以下ではこのチュートリアルのコードに潜むバグを潰していこうと思います。
index.html.org
<!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: content: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">
<script src="components/loader.js"></script>
<link rel="stylesheet" href="components/loader.css">
<link rel="stylesheet" href="css/style.css">
<script>
// 起動時のイベント
document.addEventListener('deviceready', () => {
// アプリの初期化処理
cordova.plugins.JinsMemePlugin.setAppClientID(
'', //client id
'', //secret
() => {
restartScan();
},
() => {
console.log('Error: setAppClientID');
}
);
});
// デバイスのスキャン停止
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);
setTimeout(startDataReport, 1000);
}, () => {
console.log('Disconnect');
}, () => {
console.log('Error: connect');
document.getElementById('modal').hide();
});
}
// 計測開始
const startDataReport = () => {
document.getElementById('modal').hide();
cordova.plugins.JinsMemePlugin.startDataReport(data => {
//Do somethig
}, () => {
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)`;
}
}
</script>
</head>
<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">
<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>
その前に、そもそもなぜオフィシャルのチュートリアルがうまく動かないのか?
下の約1年前の記事では、ほぼチュートリアルを参照したとのことですが、関数定義はfunction
を使っています。
MonacaとJINS MEMEでサンタさんを捕獲する - Qiita
しかし、現時点でのチュートリアルでは全てアロー関数式になっているので、恐らくコードをES6流の書き方に修正した際になにかあったのでしょう。忙しかったのかな…。中の人はお疲れ様です。
あるいは、「チュートリアルをコピペするだけではなく自ら考えよ」という意図かもしれませんね。実際勉強になりました。
といってもコードが足りない部分はやはり"正解"が欲しいところです。そんなところは「サンタさんを捕獲する」の記事(以下、「サンタさん」)を参照させていただきました。この場を借りてお礼申し上げます。
Uncaught ReferenceError: restartScan is not defined
それではひとつずつ見ていきましょう。実行して最初に出るのは「restartScan
が定義されてないよ」というエラーです。該当箇所は以下。
document.addEventListener('deviceready', () => {
// アプリの初期化処理
cordova.plugins.JinsMemePlugin.setAppClientID(
'', //client id
'', //secret
() => {
restartScan();
},
() => {
console.log('Error: setAppClientID');
}
);
});
起動時の処理なのになぜrestartScan
かというと、以下のように解説されています。
アプリの認証に成功したら、 cordova.plugins.JinsMemePlugin.startScan() でスキャンを開始します。 第一引数にデバイスが見つかった時の処理、第二引数にスキャンに失敗したときの処理を指定します。
ただし、スキャンを既に実行している場合は、一度スキャンの停止処理を行ってからスキャンを再開しないとエラーが発生します。 そこで、まず cordova.plugins.JinsMemePlugin.stopScan() を実行してからスキャンを開始しています。
という訳で、この条件を満たして書くとこうなります(サンタさん参照)。
// デバイスのスキャン再開
const restartScan = () => {
stopScan(startScan);
}
stopScan - JINS MEME DEVELOPER DOCUMENTATION
Uncaught ReferenceError: stopDataReport is not defined
次に出るのがこのエラーです。ちなみに、MonacaデバッガーはMonaca IDE上で変更後上書き保存すると自動でアプリを再起動してくれますが、その際に**JINS MEMEも再起動しないとうまく動かないことがあります。**JINS MEMEは同時に一つのアプリとしか接続できませんが、(打消し3/18:コメント欄参照)Monacaデバッガー上でアプリが再起動された場合にJINS MEMEは再起動前のアプリと接続された状態のままであるからだと思われます。
さて、今回の該当箇所はこちらです。
// アプリとデバイスの接続
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();
});
}
先程と同じパターンですが、少し疑問が生じます。すなわち、connectで接続が成功した場合の処理でまずstopDataReportを呼ぶ必要があるのか。というのも、データの計測はstartDataReportを呼び出して始める訳ですが、そのためには接続が確立している必要があります。
計測の開始前に、connect() を実行してJINS MEMEとの接続を確立する必要があります。
~~つまり、connect
成功時の処理を行う時点では、データの計測が始まっていないことは保証されているのではないでしょうか。~~そこらへんは中に人にお伺いしたいところです。(打消し3/18:コメント欄参照)
ここではとりあえず「startDataReport
の実行を1秒待つ」だけ生かして該当箇所はコメントアウトしておきます。
//setTimeout(stopDataReport, 500);
setTimeout(startDataReport, 1000);
startDataReportのsuccessCallback
ここまで直すとエラーなくアプリを実行することができます。しかしJINS MEMEを接続したところで何も起こりません。問題の箇所はこちら。
const startDataReport = () => {
document.getElementById('modal').hide();
cordova.plugins.JinsMemePlugin.startDataReport(data => {
//Do somethig
}, () => {
console.log('Error: startDataReport');
});
}
"Do something" ...。訳すると「何かやれ」ということでしょうか。無茶振りに程がありますが、幸い何をすべきかはすぐ下の書かれています。
const startDataReport = () => {
document.getElementById('modal').hide();
cordova.plugins.JinsMemePlugin.startDataReport(data => {
draw(data);
}, () => {
console.log('Error: startDataReport');
});
}
「計測結果を描画する」ですね。successCallbackの引数には計測データが格納されるようなのでdata
を渡してあげましょう。
プラグアイコン
これで想定通りの動きをするようになったはずです。しかしまだ一つ足りません。ツールバー右側のプラグアイコンです。(サンタさん参照)
<!-- ツールバー -->
<ons-toolbar>
<div class="center">JINS MEME</div>
</ons-toolbar>
<!-- ツールバー -->
<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>
reconnect()
も定義しましょう。この点はサンタさん(=以前のチュートリアルのコード)でも不十分で、きちんとdisconnectせねばなりません。
という訳でこんな感じになります。
// 再接続
const reconnect = () => {
ons.notification.confirm("再接続しますか?")
.then(result => {
if(result)
cordova.plugins.JinsMemePlugin.disconnect(restartScan, error => {
console.log('Error:reconnect ' + error.code + ' : ' + error.message);
});
})
}
(追記ここまで)
デバッグ済みコード
一つずつエラー箇所を確認しながら書いてく予定でしたが、疲れてきたので直したコードを置いときます。(追記しました)
以上、目につくバグは全て修正したのではないでしょうか!?最後に全体のコードを置いときます。不備があればコメントいただけると嬉しいです!
<!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: content: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">
<script src="components/loader.js"></script>
<link rel="stylesheet" href="components/loader.css">
<link rel="stylesheet" href="css/style.css">
<script>
// 起動時のイベント
document.addEventListener('deviceready', () => {
// アプリの初期化処理
cordova.plugins.JinsMemePlugin.setAppClientID(
'', //client id
'', //secret
() => {
restartScan();
},
() => {
console.log('Error: setAppClientID');
}
);
});
// デバイスのスキャン再開
const restartScan = () => {
stopScan(startScan);
}
// デバイスのスキャン停止
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);
setTimeout(startDataReport, 1000);
}, () => {
console.log('Disconnect');
}, () => {
console.log('Error: connect');
document.getElementById('modal').hide();
});
}
// 計測開始
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)`;
}
}
// 再接続
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">
<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>