みんなで同時に操作できる!Wio Nodeを使って自作のソーシャルIoTラジコンを作ろう

みんなで同時に操作できる!Wio Nodeを使って自作のソーシャルIoTラジコンを作ろう

いわたん

いわたん

どうも。はじめまして。DevRelチャンネル外部ライターのいわたん(@iwata_n)です。ボルダリングとロードバイクが好きなヘンジニアです:D

いきなりですが、みなさんは「100万人が16日間かけてポケモンをクリアした」という企画をご存知でしょうか? ゲーム配信ができるTwitch上で、みんながチャットで一斉にコマンドを入力し、ポケモンを進めていくという企画です。

参考記事:100万人が16日間かけて「ポケモン 赤」をクリア(早送り動画)

この、「たくさんのデータを集めて何か1つのことを実現する」というのはIoTに似ていると思いませんか? ですが、「たくさんの人からのデータを集めてモノを制御する」というのはあまり聞きません。そこに挑戦するべく、この連載では、IoTな技術を使ってたくさん人が同時に操作できるソーシャルIoTラジコンを作っていこうと思います。

今回作るもの

ブラウザから操作をすると、ラジコンが動く!というものを作ります。ブラウザから操作なので、スマホやPCさえあればみんなで操作をおこなえて、ラジコンを動かせるんです!

お買い物

ということで、さっそく必要な部材などのお買い物です。一番楽しい瞬間ですよね。いつもお財布に余裕ができると、ついついAmazonで何か面白いおもちゃが売ってないか探してしまいます。

今回は、ラジコンを作っていくので電子工作に必要そうな部材を揃えています。

DSC_0285

Wio Nodeとは?

ecbb3c05-67dc-e9fb-07da-3ee3ce3c3b75

公式の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のセットアップ
  2. モータモジュールの動作確認
  3. タミヤの工作キットでラジコンを組み立てる
  4. 操作用のWebページを作る

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はセットアップのたびに変更される値です。モータの回転方向をリセットし、回転させることができる状態にします。

モータの回転速度の変更

モータの回転方向を変更

モータのブレーキを使用

補足

こういったAPI体系なため、前進・後退といった処理をおこなう際には

  1. dcmotorX_resume で回転方向を初期化
  2. dcmotorX_change_direction でモータごとに回転方向を変更する
  3. dcmotor_speed で速度設定

といった3段階の設定(最大5リクエスト)が必要となります。(個人的には、dcmotor_speedに与えるパラメータの正負で回転方向を変更してほしかったです。そうすると1リクエストで処理が可能なためオーバーヘッドが減らせるので)

3. タミヤの工作キットでラジコンを組み立てる

では、ラジコンを組み立てていきましょう! ラジコンの本体にはタミヤの工作キットを使用します。

タミヤの工作キットは加工が簡単で安いので、DIYの味方ですね。ギアボックスとボールキャスターは説明書の通りに組み立てます

1. ユニバーサルプレートのカット

05acad59-7c10-f05d-6abe-3aa15d35d61e

ユニバーサルプレートは、ギアボックスが取り付けられるように、横14マス目、縦21マス目に切れ込みを入れてカットします。横方向はギアボックスを取り付けるため、これ以上大きくするとタイアがあたってしまい、取り付けができなくなってしまいますので注意が必要です。

(ちょっと別用で使用したプレートを使いまわしているので1箇所穴が大きいです)

2. ギアボックスを取り付ける

d3091272-c108-2381-a894-aa69d9dd1f5f

ギアボックスを取り付けるとこんな感じです。モータの端子にケーブルを半田付けしました。

3. ボールキャスターを取り付ける

aed4860c-fa97-e039-4cae-522211b834fc

さらに、ボールキャスターをつけるとこのような感じになります。

4. 配線

d21d2ab8-ba9e-5a76-8356-f07d7a3b4858

そして、基板にモータからの配線と電源からの配線を取ります。今回はUSBケーブルを切断し、モバイルバッテリーからの給電をおこなっています。

しかし、モータモジュールの仕様上は6V以上でないと駆動しないので、モバイルバッテリーの製品によっては動かない可能性があります。

5. 完成

015acad59-7c10-f05d-6abe-3aa15d35d61e

無理やり全部乗せるとこんな感じになりました。基板は落ちると危ないので、モバイルバッテリーに両面テープで固定しています。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

では、次回もお楽しみに!

この記事のシェア数

電子工作部 | 15 articles