やりたいこと
最終目標(前回と同じ)
- ([3]で完了)
二足歩行ロボット Rapiro の制御を、標準の Arduino IDE(C/C++)ベースから、JavaScript(Node.js)ベースに移植し、Rapiro を IoT デバイスっぽくする
- ([1][2]で完了)
かつ、PCでの制御ではなく、Rapiro 内部に搭載した Raspberry Pi での制御とし、完全無線化を図る - ([3]で完了)
全機能の移植が難しくとも、最低限、10個の基本動作は移植・再現する
- 可能な限り、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容も移植する
- ([4]で完了)
距離センサの搭載 - 静電容量タッチセンサの搭載など
- この Rapiro をベースに、さらに賢そうな遊び方を模索する
今回 [5] の目標
- 静電容量タッチセンサ(GROVE I2C MPR121)を搭載し、触った場所によって様々な動作をさせる
- 書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の第7章、7.5の内容に相当
方法
機材
- Rapiro
- 電源を入れる前に、こちらの手順で Arduino IDE を用いて、制御ボードに StandardFirmataPlus を書き込んでおく
- Raspberry Pi 3 Model B
- Rapiro 内部に搭載済(こちらの手順で)
- Node.js インストール済(こちらの手順で)
- npm で johnny-five, socket.io, express をインストール済
- タッチセンサ GROVE I2C MPR121
- タッチセンサ接続用の部品(接続できればよく、以下が必須というわけではない)
-
ピンヘッダ(これは必須、Rapiro のメイン基板の JP8 にはんだ付けして I2C を引き出す)
- はんだ、はんだごて
- GROVE 4ピン-ジャンパメスケーブル(メイン基板のピンとタッチセンサを接続)
-
ピンヘッダ(これは必須、Rapiro のメイン基板の JP8 にはんだ付けして I2C を引き出す)
- エネループ(単3)5本 または Rapiro 用 ACアダプタ
- PC(Windows10、Raspberry Pi に SSH や FTP できればなんでも良い)
- スマホまたはタブレット
- 無線LAN環境
- 前回から距離センサは搭載済
タッチセンサの搭載
- メイン基板の「JP8」の4ピン分の穴にピンヘッダをはんだ付け
- 下の写真がタッチセンサ
- メイン基板の JP8 にはんだ付けしたピンヘッダに、4本のワイヤを以下のように接続
ワイヤの色 | 信号 | 機能 |
---|---|---|
黄 | SCL | I2C の CLOCK 信号 |
白 | SDA | I2C の DATA 信号 |
赤 | VCC | 電源(+5V) |
黒 | GND | グランド |
- Rapiro の角?の隙間からタッチセンサの Feeler を出す
- 左右の角の裏と耳に Feeler を貼り付ける(今回は養生テープにて)
Feeler の位置 | Feeler 番号 |
---|---|
右の角 | 0 |
右の耳 | 1 |
左の角 | 2 |
左の耳 | 3 |
- 取り回しは以下のように
コーディング
概要
- 以下の三つのコードを入力し、同じ階層(e.g. /home/pi/rapiro/)に置く
- 動作の概要
- タッチセンサの値は socket で送信されて、操作インタフェース上に常に表示
- 4つの Feeler にタッチすると、様々な動作・表情を実行(今回の例は下表)
Feeler の位置 | Feeler 番号 | 動作 / 表情 |
---|---|---|
右の角 | 0 | forward / blue |
右の耳 | 1 | yellow / yellow |
左の角 | 2 | stop / white |
左の耳 | 3 | red / red |
制御プログラム本体(app.js)
前回からの主な改編部分は以下
- 32行目:タッチセンサの I2C アドレス
- 33行目:タッチセンサの Feeler の数
- 37~38行目:タッチセンサの状態を格納するプロパティ
- 42~67行目:タッチセンサの初期化の関数
- 184~229行目:タッチセンサデータの取得とタッチが検出された時の処理
app.js
// rapiro.js 05
// 静電容量タッチセンサの搭載
// 拡張用 I2C ピンの利用
// ・静電容量タッチセンサ(Grove I2C MPR121)を搭載
// ・TODO: どんな動作?
// 2017.01.28 by Mitsuteru Kokubun
// 詳しくは以下を参照してください
// http://qiita.com/mkoku/items/d559d7286ecd156f70aa
'use strict'; // 厳格モードにする
// httpサーバとsocket.ioの設定
const express = require('express'); // expressモジュールを使う
const app = express(); // expressでアプリを作る
const server = require('http').Server(app); // httpサーバを起動しアプリをサーブ
const io = require('socket.io')(server); // サーバにsocket.ioをつなぐ
server.listen(3000); // サーバの3000番ポートをリッスン開始
app.use(express.static(__dirname)); // ホームdirにあるファイルを使えるようにする
app.get('/', function (req, res) { // アクセス要求があったら
res.sendFile(__dirname + '/index.html'); // index.htmlを送る
});
let socket = null; // socket接続のインスタンス
// johnny-fiveの設定
const five = require('johnny-five'); // johnny-fiveモジュールの読み込み
const cfg = require('./rapiro-cfg'); // 設定ファイル'rapiro-cfg.js'の読み込み
const board = new five.Board({ // Rapiro制御ボードのインスタンス
port: '/dev/ttyAMA0' // シリアルポート名(環境による)
});
const pinServoDC = 17; // サーボへの電源供給ピン番号(17)
const obstacleThreshold = 9; // 障害物検出と判定する距離センサの閾値[cm]
const MPR121ADDR = 0x5A; // MPR121静電容量タッチセンサコントローラのアドレス
const feelerNum = 4; // タッチセンサのfeelerの数
const rapiro = { // Rapiroの設定や動作等を格納するオブジェクト
ready: false, // Rapiroの準備状態(初期値false)
obstacle: false, // 障害物の検出状態(初期値false)
touch: [0, 0, 0, 0], // タッチセンサのタッチ状態(初期値4ch全て0)
touched: [0, 0, 0, 0]
};
// MPR121静電容量タッチセンサコントローラの初期化関数
function mpr121Config(device) {
// Section A: dataがベースラインより大きい時のフィルタリング
device.i2cWrite(MPR121ADDR, 0x2B, 0x01); // MHD_R
device.i2cWrite(MPR121ADDR, 0x2C, 0x01); // NHD_R
device.i2cWrite(MPR121ADDR, 0x2D, 0x00); // NCL_R
device.i2cWrite(MPR121ADDR, 0x2E, 0x00); // FDL_R
// Section B: dataがベースラインより小さい時のフィルタリング
device.i2cWrite(MPR121ADDR, 0x2F, 0x01); // MHD_F
device.i2cWrite(MPR121ADDR, 0x30, 0x01); // NHD_F
device.i2cWrite(MPR121ADDR, 0x31, 0xFF); // NCL_L
device.i2cWrite(MPR121ADDR, 0x32, 0x02); // FDL_L
// Section C: 各電極(ELE0-11)の閾値(T:Touch,R:Release)の設定
device.i2cWrite(MPR121ADDR, 0x41, 0x0F); // ELE0_T
device.i2cWrite(MPR121ADDR, 0x42, 0x0A); // ELE0_R
device.i2cWrite(MPR121ADDR, 0x43, 0x0F); // ELE1_T
device.i2cWrite(MPR121ADDR, 0x44, 0x0A); // ELE1_R
device.i2cWrite(MPR121ADDR, 0x45, 0x0F); // ELE2_T
device.i2cWrite(MPR121ADDR, 0x46, 0x0A); // ELE2_R
device.i2cWrite(MPR121ADDR, 0x47, 0x0F); // ELE3_T
device.i2cWrite(MPR121ADDR, 0x48, 0x0A); // ELE3_R
// Section D: フィルタの設定
device.i2cWrite(MPR121ADDR, 0x5D, 0x04);
// Section E: 電極の設定
device.i2cWrite(MPR121ADDR, 0x5E, 0x0C);
}
// 制御ボードの準備ができたら
board.on('ready', function() {
// 各サーボのServoインスタンスを作成
rapiro.head = new five.Servo(cfg.servo.head); // 頭
rapiro.waist = new five.Servo(cfg.servo.waist); // 腰
rapiro.r_s_r = new five.Servo(cfg.servo.r_s_r); // 右肩ロール(上下)
rapiro.r_s_p = new five.Servo(cfg.servo.r_s_p); // 右肩ピッチ(開閉)
rapiro.r_h_g = new five.Servo(cfg.servo.r_h_g); // 右手
rapiro.l_s_r = new five.Servo(cfg.servo.l_s_r); // 左肩ロール(上下)
rapiro.l_s_p = new five.Servo(cfg.servo.l_s_p); // 左肩ピッチ(開閉)
rapiro.l_h_g = new five.Servo(cfg.servo.l_h_g); // 左手
rapiro.r_f_y = new five.Servo(cfg.servo.r_f_y); // 右足ヨー(開閉)
rapiro.r_f_p = new five.Servo(cfg.servo.r_f_p); // 右足ピッチ(内外)
rapiro.l_f_y = new five.Servo(cfg.servo.l_f_y); // 左足ヨー(開閉)
rapiro.l_f_p = new five.Servo(cfg.servo.l_f_p); // 左足ピッチ(内外)
// 全サーボ(全身)をServosインスタンスに入れる
rapiro.body = new five.Servos([
rapiro.head, rapiro.waist, // 頭・腰
rapiro.r_s_r, rapiro.r_s_p, rapiro.r_h_g, // 右腕
rapiro.l_s_r, rapiro.l_s_p, rapiro.l_h_g, // 左腕
rapiro.r_f_y, rapiro.r_f_p, // 右足
rapiro.l_f_y, rapiro.l_f_p // 左足
]);
// 動作アニメーションのインスタンスを作成し、Servosを紐づける
const bodyMotion = new five.Animation(rapiro.body);
// 各LEDのインスタンスを作成
rapiro.faceR = new five.Led(cfg.led.R); // 赤
rapiro.faceG = new five.Led(cfg.led.G); // 緑
rapiro.faceB = new five.Led(cfg.led.B); // 青
// 全LEDをLedsインスタンスに入れる
rapiro.face = new five.Leds([
rapiro.faceR, rapiro.faceG, rapiro.faceB
]);
// 表情アニメーションのインスタンスを作成し、Ledsを紐づける
const facialExpression = new five.Animation(rapiro.face);
// 動作アニメーションを実行する関数(引数:動作名の文字列)
rapiro.execMotion = function(motionName) {
const obj = cfg.motion; // motionオブジェクトを取得
for (let pname in obj) { // motionオブジェクト中の全プロパティについて検討
if (pname == motionName) { // 指定の動作名のプロパティがあったら
rapiro.currentMotionName = pname; // 現在の動作名をその動作名に設定
rapiro.currentMotionSequence = obj[pname]; // 現在の動作シーケンスにその動作を格納
bodyMotion.enqueue(rapiro.currentMotionSequence); // 動作アニメーションの開始
return; // あとは抜ける
}
}
console.log('Error: unidentified motion argument'); // 指定の動作名のプロパティがなかったらエラー表示
};
// 表情アニメーションを実行する関数(引数:表情名の文字列)
rapiro.execFace = function(faceName) {
const obj = cfg.face; // faceオブジェクトを取得
for (let pname in obj) { // faceオブジェクト中の全プロパティについて
if (pname == faceName) { // 指定の表情名のプロパティがあったら
rapiro.currentFaceName = pname; // 現在の表情名をその表情名に設定
rapiro.currentFaceSequence = obj[pname]; // 現在の表情シーケンスにその表情を格納
facialExpression.enqueue(rapiro.currentFaceSequence); // 表情アニメーションの開始
return; // あとは抜ける
}
}
console.log('Error: unidentified face argument'); // 指定の表情名のプロパティがなかったらエラー表示
};
// サーボ電源をON
this.pinMode(pinServoDC, five.Pin.OUTPUT); // 電源供給ピンを出力モードに
this.digitalWrite(pinServoDC, 1); // 電源供給ピンに1を出力
// 初期状態を作る
rapiro.execMotion('stop'); // 動作をstopに
rapiro.execFace('white'); // 表情をwhiteに
// Rapiroの準備OK
rapiro.ready = true;
// デバッグ用:コマンドラインからモーションと表情を制御
board.repl.inject({
rapiro: rapiro
});
console.log("Type rapiro.execMotion('name') or rapiro.execFace('name')");
// 距離センサ(GP2Y0A21YK)の処理
const proximity = new five.Proximity({
controller: 'GP2Y0A21YK', // コントローラに GP2Y0A21YK を指定
pin: 'A6', // A6 ピンを指定
freq: 250 // サンプリング間隔[ms]
});
// 距離センサのデータが取得されたら
proximity.on('data', function() {
const dist = this.cm; // cm or in(inch)
// 障害物の処理
if ((dist < obstacleThreshold) && (rapiro.obstacle == false)) {
// 距離が閾値未満かつ現在障害物が未検出状態だったら
rapiro.obstacle = true; // 障害物検出状態に
rapiro.previousMotionName = rapiro.currentMotionName; // 現在の動作名を一時保管
rapiro.previousFaceName = rapiro.currentFaceName; // 現在の表情名を一時補間
rapiro.execMotion('blue'); // 動作blueを実行
rapiro.execFace('red'); // 表情redを実行
} else if ((dist >= obstacleThreshold) && (rapiro.obstacle == true)) {
// 距離が閾値以上かつ現在障害物が検出状態だったら
rapiro.obstacle = false; // 障害物未検出状態に
rapiro.execMotion(rapiro.previousMotionName); // 前の動作を再開
rapiro.execFace(rapiro.previousFaceName); // 前の表情を再開
}
// 距離の値と現状の動作・表情をsocketで送る
emitSocket('proximity', {
distance: dist // 距離
});
emitSocket('response', {
motion: rapiro.currentMotionName, // 動作名
face: rapiro.currentFaceName // 表情名
});
});
// タッチセンサ(Grove I2C MPR121)の処理
this.i2cConfig(); // I2Cを使うためのjohnny-fiveのおまじない
mpr121Config(this); // MPR121静電容量タッチセンサコントローラを初期設定
// タッチセンサの状態の読み込みとタッチの判定(i2cRead()は繰り返し実行される)
this.i2cRead(MPR121ADDR, 1, function(bytes){
(bytes & 0x01) == 0x01 ? rapiro.touch[0] = 1 : rapiro.touch[0] = 0; // ch0を判定
(bytes & 0x02) == 0x02 ? rapiro.touch[1] = 1 : rapiro.touch[1] = 0; // ch1を判定
(bytes & 0x04) == 0x04 ? rapiro.touch[2] = 1 : rapiro.touch[2] = 0; // ch2を判定
(bytes & 0x08) == 0x08 ? rapiro.touch[3] = 1 : rapiro.touch[3] = 0; // ch3を判定
for (let t = 0; t < feelerNum; t++) {
if ((rapiro.touch[t] == 1) && (rapiro.touched[t] == 0)) {
// タッチセンサに触れた時(触れていなかった状態から)
rapiro.touched[t] = 1;
switch (t) {
case 0:
rapiro.execMotion('forward'); // 動作forwardを実行
rapiro.execFace('blue'); // 表情blueを実行
break;
case 1:
rapiro.execMotion('yellow'); // 動作yellowを実行
rapiro.execFace('yellow'); // 表情yellowを実行
break;
case 2:
rapiro.execMotion('stop'); // 動作stopを実行
rapiro.execFace('white'); // 表情whiteを実行
break;
case 3:
rapiro.execMotion('red'); // 動作red実行
rapiro.execFace('red'); // 表情redを実行
break;
}
}
if ((rapiro.touch[t] == 0) && (rapiro.touched[t] == 1)) {
// タッチセンサから離した時(触れた状態から)
rapiro.touched[t] = 0;
}
}
// タッチセンサの値と現状の動作・表情をsocketで送る
emitSocket('touch', {
touch: rapiro.touch // タッチセンサの状態
});
emitSocket('response', {
motion: rapiro.currentMotionName, // 動作名
face: rapiro.currentFaceName // 表情名
});
});
// 終了時の処理
this.on('exit', function() {
this.digitalWrite(pinServoDC, 0); // サーボの電源をOFF
});
});
// WebSocketによる制御
io.on('connection', function(s) {
socket = s; // socket接続有り
socket.on('request', function(data) { // requestイベントが届いたら
if (rapiro.ready == true) { // Rapiroの準備OKなら
// アニメーションを実行
rapiro.execMotion(data.motion); // 動作アニメーション
rapiro.execFace(data.face); // 表情アニメーション
// 現在の状態をresponseイベントとしてsocketで送る
emitSocket('response', {
motion: rapiro.currentMotionName, // 動作名
face: rapiro.currentFaceName // 表情名
});
}
});
});
// ブラウザにsocketでデータを送る
function emitSocket(event, data) {
if (socket != null) {
socket.emit(event, data);
}
}
操作インタフェース(index.html)
前回からの主な改編部分は以下
- 30~33行目:タッチセンサの値を表示する場所
- 157~161行目:タッチセンサの値を表示
index.html
<!DOCTYPE html>
<!--
// Rapiro制御インタフェース
// Rapiro標準の10モーション実装
// socket.ioによりブラウザから制御
// 距離センサ(GP2Y0A21YK)による動作制御
// タッチセンサ(Grove I2C MPR121)による動作制御
// 2017.01.28 by Mitsuteru Kokubun
-->
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>rapiro.js 05: タッチセンサ</title>
<style>
td.center {text-align: center}
</style>
</head>
<body>
<table>
<!-- Rapiroからのレスポンスを表示する場所 -->
<tr>
<td class='center' colspan='3' id='msg'>motion / face</td>
</tr>
<!-- Rapiroの距離センサの値を表示する場所 -->
<tr>
<td class='center' colspan='3' id='prox'>proximity [cm]</td>
</tr>
<!-- Rapiroのタッチセンサの値を表示する場所 -->
<tr>
<td class='center' colspan='3' id='touch'>touch [0, 1, 2, 3]</td>
</tr>
<tr><td> </td></tr>
<!-- 操作ボタン -->
<tr>
<td class='center'></td>
<td class='center'><input type='button' value='↑' id='btnForward'></td>
<td class='center'></td>
</tr>
<tr>
<td class='center'><input type='button' value='←' id='btnLeft'></td>
<td class='center'><input type='button' value='■' id='btnStop'></td>
<td class='center'><input type='button' value='→' id='btnRight'></td>
</tr>
<tr>
<td class='center'></td>
<td class='center'><input type='button' value='↓' id='btnBack'></td>
<td class='center'></td>
</tr>
<tr><td> </td></tr>
<tr>
<td class='center'><input type='button' value='R' id='btnRed'></td>
<td class='center'><input type='button' value='G' id='btnGreen'></td>
<td class='center'><input type='button' value='B' id='btnBlue'></td>
</tr>
<tr>
<td class='center'><input type='button' value='Y' id='btnYellow'></td>
<td class='center'><input type='button' value='P' id='btnPush'></td>
<td class='center'></td>
</tr>
</table>
<!-- socket.ioライブラリの読み込み(定型) -->
<script src='/socket.io/socket.io.js'></script>
<!-- メインスクリプト -->
<script>
var socket = io('http://192.168.43.200:3000'); // socket接続
// 各ボタンオブジェクトの取得
var btnStop = document.getElementById('btnStop'); // stopボタン
var btnForward = document.getElementById('btnForward'); // forwardボタン
var btnBack = document.getElementById('btnBack'); // backボタン
var btnRight = document.getElementById('btnRight'); // rightボタン
var btnLeft = document.getElementById('btnLeft'); // leftボタン
var btnGreen = document.getElementById('btnGreen'); // greenボタン
var btnYellow = document.getElementById('btnYellow'); // yellowボタン
var btnBlue = document.getElementById('btnBlue'); // blueボタン
var btnRed = document.getElementById('btnRed'); // redボタン
var btnPush = document.getElementById('btnPush'); // pushボタン
// 各ボタンをクリックした時の処理
btnStop.addEventListener('click', function() { // stop
socket.emit('request', { // requestイベントをsocketで送る
motion: 'stop', // 動作名
face: 'white' // 表情名
});
});
btnForward.addEventListener('click', function() { // forward
socket.emit('request', {
motion: 'forward',
face: 'blue'
});
});
btnBack.addEventListener('click', function() { // back
socket.emit('request', {
motion: 'back',
face: 'blue'
});
});
btnRight.addEventListener('click', function() { // right
socket.emit('request', {
motion: 'right',
face: 'blue'
});
});
btnLeft.addEventListener('click', function() { // left
socket.emit('request', {
motion: 'left',
face: 'blue'
});
});
btnGreen.addEventListener('click', function() { // green
socket.emit('request', {
motion: 'green',
face: 'green'
});
});
btnYellow.addEventListener('click', function() { // yellow
socket.emit('request', {
motion: 'yellow',
face: 'yellow'
});
});
btnBlue.addEventListener('click', function() { // blue
socket.emit('request', {
motion: 'blue',
face: 'blue'
});
});
btnRed.addEventListener('click', function() { // red
socket.emit('request', {
motion: 'red',
face: 'red'
});
});
btnPush.addEventListener('click', function() { // push
socket.emit('request', {
motion: 'push',
face: 'blue'
});
});
// Rapiroからのレスポンスの表示
socket.on('response', function(data) { // socketでresponseイベントが届いたら
var msg = document.getElementById('msg'); // div要素を取得
msg.innerHTML = data.motion + ' / ' + data.face; // 動作と表情を表示
});
// Rapiroの距離センサの値の表示
socket.on('proximity', function(data) {
var prox = document.getElementById('prox');
prox.innerHTML = data.distance + ' cm'
});
// Rapiroのタッチセンサの値の表示
socket.on('touch', function(data) {
var touch = document.getElementById('touch');
touch.innerHTML = data.touch
});
</script>
</body>
</html>
動作確認
- 上記3つのコードを Rapiro 内部の Raspberry Pi に FTP
- e.g. /home/pi/rapiro/ 内に
- SSH で Rapiro 内部の Raspberry Pi にアクセスし、node で app.js を実行
- 準備ができると Rapiro のサーボに通電され直立状態となり、目は白色にフェードする
pi@raspberrypi:~/rapiro $ node app.js
- 各 Feeler に触れると、所定の動作・表情が実行されればOK
- 以下、動作の様子(動画)
- PC や スマホのブラウザで、Raspberry Pi のIPアドレスにアクセス
http://192.168.**.***:3000
- タッチの状態が表示されていればOK
まとめ
これで、最終目標の 2 を達成できました。ついに、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容をほぼ移植できました(サウンドディテクタと XBee は除く)。
本当はこの書籍のように Rapiro の内部に Feeler を隠したかったのですが、試したところ、頭部の内側に Feeler を貼った状態では反応しませんでした。角の裏側に貼った状態ならかろうじて(感度は悪いですが)反応してくれました。もしかすると初期設定のパラメータなどで感度を変えられるのかもしれませんが、調べていません。今後の(長期的な)課題とします。
次回以降は、世の中にある Rapiro の楽しげな改造事例を参考にしながら、例えば以下のようなことに挑戦していきたい考えです(あくまで考え)。
- スピーカーの搭載、音声合成
- 天気予報
- 赤外線による家電の操作
- カメラの搭載、ストリーミング
- 音声認識
- 人工知能?による簡単な会話
- etc.
最新コードは以下にあります。
https://github.com/mkokubun/rapiro.js