どうも。はじめまして。DevRelチャンネル外部ライターのいわたん(@iwata_n)です。ボルダリングとロードバイクが好きなヘンジニアです:D
いきなりですが、みなさんは「100万人が16日間かけてポケモンをクリアした」という企画をご存知でしょうか? ゲーム配信ができるTwitch上で、みんながチャットで一斉にコマンドを入力し、ポケモンを進めていくという企画です。
参考記事:100万人が16日間かけて「ポケモン 赤」をクリア(早送り動画)
この、「たくさんのデータを集めて何か1つのことを実現する」というのはIoTに似ていると思いませんか? ですが、「たくさんの人からのデータを集めてモノを制御する」というのはあまり聞きません。そこに挑戦するべく、この連載では、IoTな技術を使ってたくさん人が同時に操作できるソーシャルIoTラジコンを作っていこうと思います。
今回作るもの
ブラウザから操作をすると、ラジコンが動く!というものを作ります。ブラウザから操作なので、スマホやPCさえあればみんなで操作をおこなえて、ラジコンを動かせるんです!
お買い物
ということで、さっそく必要な部材などのお買い物です。一番楽しい瞬間ですよね。いつもお財布に余裕ができると、ついついAmazonで何か面白いおもちゃが売ってないか探してしまいます。
今回は、ラジコンを作っていくので電子工作に必要そうな部材を揃えています。
- Wio Node
- IoTに特化したECサイト dotstudio で購入できます
- Grove I2C モータードライバモジュール
- タミヤの工作キット
- より線
- ハンダ
- 半田ごて、半田
- 精密ドライバー(マイナス)
Wio Nodeとは?
公式のWikiによると、SoC(System On a Chip)にはESP8266を採用し、無線LANを搭載しているとのこと。また、「Wio Link」というアプリで、プログラミング・ブレッドボード・半田付けなしでIoTデバイスを開発できるそうです。
つまり、かなり簡単にIoTデバイスの開発ができるようなガジェットですね。ただ、実際にモノを作るときにはブレッドボードとハンダはあったほうが良いです。
Wikiに載っているスペックをまとめると以下のとおりです。
項目 | スペック |
---|---|
Wi-Fi規格 | 802.11b/g/n |
Wi-Fi暗号化 | WEP/TKIP/AES |
外部インターフェース | UART0/I2C0/D0, Analog/I2C1/D1(両方ともGroveコネクタ) |
I/Oピンからの出力電流 | 12mA |
入力電圧 | マイクロUSBからは5V、バッテリからは3.4~4.2V |
最大出力電流 | 1000mA |
駆動電圧 | 3.3V |
最大充電電流 | 500mA |
フラッシュメモリ | 4MByte(W25Q32B) |
寸法 | 28mm x 28mm |
CPUクロック | 26MHz |
CE/FCC/TELEC 認証 | ESP-WROOM-02(only) |
それでは早速始めていきましょう! 順序は以下の通りです。
1. Wio Nodeのセットアップ
まずは、Wio Nodeの「UART/I2C0/D0」と書かれた側の端子と、モータドライバモジュールの「I2C」と書かれた端子をコネクタで接続します。公式Wikiの作業手順(英語)に沿ってセットアップをしていきます。
1-1. スマートフォンにアプリをインストールする
「Wio Link」というWio Node用のアプリをスマートフォンにインストールします。このアプリでは、Wio Nodeの設定やファームウェア(内部で動作するプログラム)の書き込み、Wio Nodeを制御するためのURLを取得できます。
Wio Linkは、Android 4.1以上向けとiOS 7以上向けにリリースされています。以下からダウンロードしてください。
・Android – Google Play
・iOS – Apple Store
今回はAndroid版を利用して紹介していきます。
1-2. アカウントの作成
初めてWio Linkを使用するときはアカウントの作成が必要です。Wio Linkのアプリを起動すると以下のような画面が表示されます。ここではSING UPを押します。
メールアドレスとパスワードを設定します。
1-3. Wio Nodeとの接続
Wio Nodeの設定ボタンを4秒以上長押しすることで、Wio Nodeを設定モードに変更できます。設定モードになると、Wio Nodeの青色LEDの明るさが周期的に変化する状態になります。
この状態でスマホアプリ側の「+」ボタンを押して、「Setup Wio Node」を選択します。
「GOT READY」を押します。
そうすると、近くで設定モードになっているWio Nodeの一覧が表示されます。設定をおこなうWio Nodeを選びタップをしましょう。
次に、周辺で飛んでいるWi-Fiの一覧が表示されるので、Wio Nodeを接続したいWi-FiのSSIDを選択します。
パスワードと識別名を付けます。
このときに、なかなかWio NodeがIPアドレスを取得できず、タイムアウトを起こすことがあります。その際は、「設定中」が表示されている間に、先ほど選んだWio Nodeが接続するWi-Fiにスマートフォン側のWi-Fiを接続し直すと、設定が完了することがあります。
うまく設定できないことが多々あるのですが、何度か繰り返すと設定できたりと少し不安定なところがあるので、何度か試してみてください。
1-4. モジュールを設定する
Wio Nodeに接続されているモジュールを、画面下に表示されている一覧からドラッグ&ドロップで選択していきます。
たまにセンサー等で「Input」の中にないものが有りますが、そういったセンサーはGeneric Analog Inputで動きます。今回はラジコンを作るために、モーターを制御するモータモジュールを接続します。
1-5. ファームウェアのアップデート
Wio Nodeで動作するプログラム(ファームウェア)をアップデートします。画面右上にある「Download」をタップすることで、自動的にファームウェアがダウンロードされます。プログラムを書く必要はありません。
1-6. APIのテスト
ファームウェアのダウンロードが完了したら、画面右上の「API」からテスト用のAPIを試せる画面に移ります。ここでは、先ほど設定したモジュールごとにWebAPIが提供されており、それらのWeb APIにアクセスすると、センサーの値が読めたり、LEDの点滅を制御したりすることができるんです。
つまり、Wio Nodeを使うと、スマートフォンだけでセンサやLEDの制御をおこなえてしまいます。また、普通にAJAX通信などでWeb APIにアクセスするプログラムを書いてしまえば、簡単にブラウザから制御できるIoTデバイスを作ることも可能です。
2. モータモジュールの動作確認
セットアップが完了したら決まった、URLを使って動作確認をおこなっていきましょう。このときはまだモータを接続しなくても、モータモジュール上のLEDだけで簡単な動作確認ができます。
モータモジュールには以下のAPIが用意されています。
回転方向のリセット&Lチカ
LEDが回転方向に合わせて光ります。access_tokenの後ろのXXXXXXはセットアップのたびに変更される値です。モータの回転方向をリセットし、回転させることができる状態にします。
- https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C0/dcmotor1_resume?access_token=XXXXXX
- https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C0/dcmotor2_resume?access_token=XXXXXX
モータの回転速度の変更
モータの回転方向を変更
- https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C0/dcmotor1_change_direction?access_token=XXXXXX
- https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C0/dcmotor2_change_direction?access_token=XXXXXX
モータのブレーキを使用
- https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C0/dcmotor1_break?access_token=XXXXXX
- https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C0/dcmotor2_break?access_token=XXXXXX
補足
こういったAPI体系なため、前進・後退といった処理をおこなう際には
dcmotorX_resume
で回転方向を初期化dcmotorX_change_direction
でモータごとに回転方向を変更するdcmotor_speed
で速度設定
といった3段階の設定(最大5リクエスト)が必要となります。(個人的には、dcmotor_speedに与えるパラメータの正負で回転方向を変更してほしかったです。そうすると1リクエストで処理が可能なためオーバーヘッドが減らせるので)
3. タミヤの工作キットでラジコンを組み立てる
では、ラジコンを組み立てていきましょう! ラジコンの本体にはタミヤの工作キットを使用します。
- 楽しい工作シリーズ No.172 ユニバーサルプレートL 210×160mm (70172)
- 楽しい工作シリーズ No.168 ダブルギヤボックス 左右独立4速タイプ (70168)
- 楽しい工作シリーズ No.144 ボールキャスター 2セット入 (70144)
タミヤの工作キットは加工が簡単で安いので、DIYの味方ですね。ギアボックスとボールキャスターは説明書の通りに組み立てます。
1. ユニバーサルプレートのカット
ユニバーサルプレートは、ギアボックスが取り付けられるように、横14マス目、縦21マス目に切れ込みを入れてカットします。横方向はギアボックスを取り付けるため、これ以上大きくするとタイアがあたってしまい、取り付けができなくなってしまいますので注意が必要です。
(ちょっと別用で使用したプレートを使いまわしているので1箇所穴が大きいです)
2. ギアボックスを取り付ける
ギアボックスを取り付けるとこんな感じです。モータの端子にケーブルを半田付けしました。
3. ボールキャスターを取り付ける
さらに、ボールキャスターをつけるとこのような感じになります。
4. 配線
そして、基板にモータからの配線と電源からの配線を取ります。今回はUSBケーブルを切断し、モバイルバッテリーからの給電をおこなっています。
しかし、モータモジュールの仕様上は6V以上でないと駆動しないので、モバイルバッテリーの製品によっては動かない可能性があります。
5. 完成
無理やり全部乗せるとこんな感じになりました。基板は落ちると危ないので、モバイルバッテリーに両面テープで固定しています。Wio Nodoの弱点として、小さい代わりに固定用の穴がないため、固定するためには何かしらの工夫が必要になります。
モータモジュールには固定用の穴があるのですが、今回作成したラジコンだとスペースが足りなかったので、このような手作り感溢れる実装となりました。
動作確認
ここまでできたら簡単な動作確認をしてみます。
グルグルとまわしてみました。問題なく動いています!
4. 操作用のWebページを作る
最後に、操作用のWebページを作ります。
HTML(index.html)
HTMLはこちらです。
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div style='margin: 5px'>
<button type="button" id='forward'>前進</button>
<button type="button" id='stop'>停止</button>
<button type="button" id='back'>後退</button>
<button type="button" id='right'>右旋回</button>
<button type="button" id='left'>左旋回</button>
<div id='timeline'></div>
</div>
<script src='https://cdn.mlkcca.com/v2.0.0/milkcocoa.js'></script>
<script src="demo.js"></script>
<script src="https://code.jquery.com/jquery-3.0.0.min.js" integrity="sha256-JmvOoLtYsmqlsWxa7mDSLMwa6dZ9rrIdtrrVYRnDRH0=" crossorigin="anonymous"></script>
</body>
</html>
JavaScript
JavaScriptは以下のようになります。
// ウィンドウの読み込み完了
window.onload = () => {
// Milkcocoa
const milkcocoa = new MilkCocoa('oniolavi4s.mlkcca.com');
const ds = milkcocoa.dataStore('command');
// APIのテストで確認したトークン
const ACCESS_TOKEN = '1591919673ed0affc8d0298ca5fa1176';
// ボタン
const forward = document.getElementById('forward');
const stop = document.getElementById('stop');
const back = document.getElementById('back');
const left = document.getElementById('left');
const right = document.getElementById('right');
// コマンド履歴
const timeline = document.getElementById('timeline');
// 各ボタンを押した時のコールバックを定義
forward.addEventListener('click', (e) => {
const sequence = [
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/0/0?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_resume?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_change_direction?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor2_resume?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/255/255?access_token=' + ACCESS_TOKEN,
];
sequence.reduce((promise, value) => {
return promise.then(() => {
return $.post({url: value}).then((data)=>console.log(data));
});
}, Promise.resolve())
.then(ds.send({command: '前進'}));
});
stop.addEventListener('click', (e) => {
const sequence = [
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/0/0?access_token=' + ACCESS_TOKEN,
];
sequence.reduce((promise, value) => {
return promise.then(() => {
return $.post({url: value}).then((data)=>console.log(data));
});
}, Promise.resolve())
.then(ds.send({command: '停止'}));
});
back.addEventListener('click', (e) => {
const sequence = [
$.post({ url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/0/0?access_token=' + ACCESS_TOKEN }),
$.post({ url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_resume?access_token=' + ACCESS_TOKEN }),
$.post({ url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor2_resume?access_token=' + ACCESS_TOKEN }),
$.post({ url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor2_change_direction?access_token=' + ACCESS_TOKEN }),
$.post({ url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/255/255?access_token=' + ACCESS_TOKEN }),
];
sequence.reduce((promise, value) => {
return promise.then(() => {
return $.post({url: value}).then((data)=>console.log(data));
});
}, Promise.resolve())
.then(ds.send({command: '後退'}));
});
left.addEventListener('click', (e) => {
const sequence = [
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/0/0?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_resume?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_change_direction?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor2_resume?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor2_change_direction?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/255/255?access_token=' + ACCESS_TOKEN,
];
sequence.reduce((promise, value) => {
return promise.then(() => {
return $.post({url: value}).then((data)=>console.log(data));
});
}, Promise.resolve())
.then(ds.send({command: '左回転'}));
});
right.addEventListener('click', (e) => {
const sequence = [
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/0/0?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_resume?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor2_resume?access_token=' + ACCESS_TOKEN,
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/255/255?access_token=' + ACCESS_TOKEN,
];
sequence.reduce((promise, value) => {
return promise.then(() => {
return $.post({url: value}).then((data)=>console.log(data));
});
}, Promise.resolve())
.then(ds.send({command: '右回転'}));
});
// Milkcocoaからデータが送られてきた時のコールバック
ds.on('send', (data) => {
console.log(data.value);
// 送られてきたデータをコマンド履歴の一番最初に追加する
const child = document.createElement('div');
child.innerHTML = data.value['command']
timeline.insertBefore(child, timeline.firstChild);
})
}
解説
細かい部分を解説してみようと思います。
// Milkcocoa
const milkcocoa = new MilkCocoa('oniolavi4s.mlkcca.com');
const ds = milkcocoa.dataStore('command');
/* 略 */
// Milkcocoaからデータが送られてきた時のコールバック
ds.on('send', (data) => {
console.log(data.value);
// 送られてきたデータをコマンド履歴の一番最初に追加する
const child = document.createElement('div');
child.innerHTML = data.value['command']
timeline.insertBefore(child, timeline.firstChild);
})
まずはじめに、リアルタイムでのデータのやり取りや保存ができるクラウドプラットフォームMilkcocoaを利用して、Push通知の実装をおこないます。Milkcocoaを利用することで、各ブラウザへの通知がサーバレスで簡単に実現できます。
ほぼサンプルの通りなのですが、Milkcocoaのインスタンスを作成し、データストアへの参照を用意します。そうすると ds.on('メソッド名', function(){})
で送信されたデータの監視をおこなえるんです。ここで他の人の操作を受け取り、画面に表示します。
// APIのテストで確認したトークン
const ACCESS_TOKEN = 'ebbaec233567949b8accb33e80ccf3b3';
次に、Wio Nodeの操作アプリでAPIのテストの際に表示されていたアクセストークン部分を定数として用意します。これは、Wio Nodeをセットアップするたびに変更される値となるので、自分で用意したWio Nodeのアクセストークンとなるように変更をしましょう。
// ボタン
const forward = document.getElementById('forward');
const stop = document.getElementById('stop');
const back = document.getElementById('back');
const left = document.getElementById('left');
const right = document.getElementById('right');
// コマンド履歴
const timeline = document.getElementById('timeline');
HTMLのDOMからボタンなどの要素を取得します。これらに対してJavaScriptから操作などをし、画面表示やボタン入力を受付ます。
// 各ボタンを押した時のコールバックを定義
forward.addEventListener('click', (e) => {
const sequence = [
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/0/0?access_token=' + ACCESS_TOKEN, // 両方のモータの速度を0にする
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_resume?access_token=' + ACCESS_TOKEN, // モータ1をリセット
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_change_direction?access_token=' + ACCESS_TOKEN, // モータ1の回転方向を逆転
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor2_resume?access_token=' + ACCESS_TOKEN, // モータ2をリセット
'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/255/255?access_token=' + ACCESS_TOKEN, // 両方のモータの速度を255にする
];
sequence.reduce((promise, value) => {
return promise.then(() => {
return $.post({url: value}).then((data)=>console.log(data));
});
}, Promise.resolve())
.then(ds.send({command: '前進'}));
});
ボタンを押した時の挙動を定義します。jQueryを利用してWio NodeのWebAPIへリクエストを送信します。
// これはダメ
forward.addEventListener('click', (e) => {
$.post({url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/0/0?access_token=' + ACCESS_TOKEN,});
$.post({url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_resume?access_token=' + ACCESS_TOKEN,});
$.post({url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor1_change_direction?access_token=' + ACCESS_TOKEN,});
$.post({url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor2_resume?access_token=' + ACCESS_TOKEN,});
$.post({url: 'https://iot.seeed.cc/v1/node/GroveI2CMotorDriverI2C1/dcmotor_speed/255/255?access_token=' + ACCESS_TOKEN,});
}
ここで注意なのが、jQueryのAJAX通信は非同期な処理のため、連続してリクエストをおこなうと、処理の順番が変わってしまったりして挙動がおかしくなります。
今回は順番にリクエストをするためにはPromiseを利用しています。
Promiseに関してはazu氏の JavaScript Promiseの本 に詳しく書かれていますので、そちらを参考にしていただけると良いかと思います。それぞれの移動方向に合わせてモータの回転方向を設定します。モータと基板の配線によっては回転方向が逆になっている可能性もありますので、適宜読み替えてみてください。
これで、index.htmlをブラウザで開くと、下のような画面が表示されます。これで、「前進」や「後退」などのボタンを押すとモータが回転し自作したラジコンが動き出します! HTMLとJavaScriptでできているので、Github Pagesなどに上げることでそのまま世界中の人たちと一緒に操作することも可能です。
おわりに
Wio Nodeを使うと、簡単にソーシャルIoTラジコンを作ることができました。ほぼ半田付け作業も不要です。今回はモータを回すためにGrove I2C モータードライバモジュールを使用しましたが、少しプログラミングが複雑になってしまいました。
しかし、Wio Nodeには他にもIFTTT連携も標準で用意されており、センサ値をトリガーにしてTwitterに投稿をするというのも簡単にできるようです。次回以降はこういった内容も紹介していきたいと思います。
また、Wio NodeはIoTに特化したECサイト dotstudio から購入できます。ぜひゲットしてみてください。
今回の内容では、裏ではいろいろなハードウェアの技術が使われています。もし興味がありましたら参考資料も見てみてください。
- 技術的な参考資料
-
今回作成した回路で使われている技術で参考になる資料です。
・I2C
Wio Nodeとモータモジュール間の通信で使用されている規格
http://www.picfun.com/c15.html・Hブリッジ
単一電源でモータを正転・逆転・ブレーキを行うための回路
http://www.picfun.com/motor03.html
では、次回もお楽しみに!