やりたいこと
最終目標(前回と同じ)
- 二足歩行ロボット Rapiro の制御を、標準の Arduino IDE(C/C++)ベースから、JavaScript(Node.js)ベースに移植し、Rapiro を IoT デバイスっぽくする
- ([1]で完了)
かつ、PCでの制御ではなく、Rapiro 内部に搭載した Raspberry Pi での制御とし、完全無線化を図る - 全機能の移植が難しくとも、最低限、10個の基本動作は移植・再現する
- 可能な限り、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容も移植する
- 距離センサの搭載
- 静電容量タッチセンサの搭載など
- この Rapiro をベースに、さらに賢そうな遊び方を模索する
今回 [2] の目標
- 全てのサーボとLEDを自由に制御する基礎を作る
- ポージングさせるだけ
- 歩行のような連続動作は行わない
- ブラウザから操作する
- LAN内限定だが、スマホからでも操作
- Rapiroの電源以外は無線
- IoTっぽい
方法
機材(前回とほぼ同じ)
- Rapiro
- 電源を入れる前に、Arduino IDE を用いて、制御ボードに StandardFirmata を書き込んでおく
- いつでも Rapiro の標準ファームウェアに戻せるので問題なし
- Raspberry Pi 3 Model B
- Rapiro 内部に搭載済(こちらの手順で)
- Node.js インストール済(こちらの手順で)
- Rapiro 用 ACアダプタ
- PC(Windows10、Raspberry Pi に SSH や FTP できればなんでも良い)
- スマホまたはタブレット
- 無線LAN環境
手続き
準備
- Rapiro の標準ファームウェア(Arduino のスケッチ)や書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」を参考に、Rapiro の制御アルゴリズムを勉強する
- Rapiro 内の Raspberry Pi に SSH でアクセスし、npm で johnny-five、socket.io、express をインストール
pi@raspberrypi:~ $ npm install johnny-five socket.io express
コーディング
- 制御プログラム本体(app.js)と、操作インタフェース(index.html)を以下のように書く(ものすごい見にくいコードだが、とりあえず動いたので良し)
- 例として、/home/pi/rapiro/pose_test 内に作成
- 実際には、PC の エディタ(Microsoft Visual Studio Code)で書いて、Raspberry Pi に FTP しました
app.js
// Rapiro制御ボードをNode.jsで制御
// Raspberry Pi 版 2017.01.09 by mkoku
// ・ポージングする
// ・socket.ioでhtmlから制御
'use strict'; // 厳格モードにする
// httpサーバとsocket.ioの設定
const express = require('express'); // expressモジュールを使う
const app = express(); // expressでアプリを作る
const server = require('http').Server(app);
const io = require('socket.io')(server);
server.listen(3000);
app.use(express.static(__dirname)); // ホームdirにあるファイルを使えるようにする
app.get('/', function (req, res) { // アクセス要求があったら
res.sendFile(__dirname + '/index.html'); // index.htmlを送る
});
// johnny-fiveの設定
const five = require('johnny-five'); // johnny-fiveモジュールを使う
const rapiro = new five.Board({ // Rapiro制御ボードを取得
port: '/dev/ttyAMA0' // ポート名(環境による)
});
let rapiroReady = false;
// グローバル変数
const SVONUM = 12; // サーボの個数
let servo = []; // サーボアレイオブジェクト
const pinServoDC = 17; // サーボに電源供給しているピン番号(17=A3ピン)
const LEDNUM = 3; // LEDの個数(RGB)
let led = []; // LEDアレイオブジェクト
// trimオブジェクト
const trim = {
name: 'trim',
pose: {
headYaw: -7,
waistYaw: 2,
rightShoulderPitch: 0,
rightShoulderRoll: 0,
rightHandOpen: 0,
leftShoulderPitch: 0,
leftShoulderRoll: 10,
leftHandOpen: 0,
rightLegYaw: -10,
rightFootRoll: 6,
leftLegYaw: 9,
leftFootRoll: -12
},
led: {
R: 0,
G: 0,
B: 0
},
timeInMs: 0
};
// initialオブジェクト
const initial = {
name: 'Initial Status',
pose: {
headYaw: 90,
waistYaw: 90,
rightShoulderPitch: 0,
rightShoulderRoll: 130,
rightHandOpen: 90,
leftShoulderPitch: 180,
leftShoulderRoll: 40,
leftHandOpen: 90,
rightLegYaw: 90,
rightFootRoll: 90,
leftLegYaw: 90,
leftFootRoll: 90
},
led: {
R: 127,
G: 127,
B: 127
},
timeInMs: 500
};
// オブジェクト変数を配列変数に変換する関数
function objToArray(obj) {
let arr = [];
arr[0] = obj.pose.headYaw;
arr[1] = obj.pose.waistYaw
arr[2] = obj.pose.rightShoulderPitch;
arr[3] = obj.pose.rightShoulderRoll;
arr[4] = obj.pose.rightHandOpen;
arr[5] = obj.pose.leftShoulderPitch;
arr[6] = obj.pose.leftShoulderRoll;
arr[7] = obj.pose.leftHandOpen;
arr[8] = obj.pose.rightLegYaw;
arr[9] = obj.pose.rightFootRoll;
arr[10] = obj.pose.leftLegYaw;
arr[11] = obj.pose.leftFootRoll;
arr[12] = obj.led.R;
arr[13] = obj.led.G;
arr[14] = obj.led.B;
arr[15] = obj.timeInMs;
return arr;
}
// ポージング関数
function posing(poseObj) {
let poseArray = [];
let trimArray = [];
poseArray = objToArray(poseObj);
trimArray = objToArray(trim);
// console.log(poseObj);
// 各サーボを指定ポーズに動かす
for (let s = 0; s < SVONUM; s++) {
servo[s].to(poseArray[s] + trimArray[s], poseArray[15]);
}
// 各LEDを指定の明るさ・色にする
for(let l = 0; l < LEDNUM; l++) {
led[l].fade(poseArray[l+SVONUM], poseArray[15]);
}
}
// Rapiroの初期設定
rapiro.on('ready', function() { // Rapiro制御ボードがreadyなら
// サーボの接続
servo[0] = new five.Servo(10); // 0: 頭部・回転 (左0 90 右180)
servo[1] = new five.Servo(11); // 1: 腰部・回転 (左0 90 右180)
servo[2] = new five.Servo(9); // 2: 右肩・上下 (上180 0 下0)
servo[3] = new five.Servo(8); // 3: 右肩・開閉 (開40 130 閉130)
servo[4] = new five.Servo(7); // 4: 右手・開閉 (開120 90 閉70)
servo[5] = new five.Servo(12); // 5: 左肩・上下 (上0 180 下180)
servo[6] = new five.Servo(13); // 6: 左肩・開閉 (開130 40 閉40)
servo[7] = new five.Servo(14); // 7: 左手・開閉 (開70 90 閉120)
servo[8] = new five.Servo(4); // 8: 右脚・回転 (外股180 90 内股0)
servo[9] = new five.Servo(2); // 9: 右足・捻り (外裏180 90 内裏0)
servo[10] = new five.Servo(15); // 10: 左脚・回転 (外股0 90 内股180)
servo[11] = new five.Servo(16); // 11: 左足・捻り (外裏0 90 内裏180)
// LEDの接続
led = new five.Leds([6, 5, 3]); // [R, G, B]
// サーボへの電源供給開始
this.pinMode(pinServoDC, five.Pin.OUTPUT)
this.digitalWrite(pinServoDC, 1);
// 初期状態にする
posing(initial);
rapiroReady = true;
// console.log('Rapiro is ready!');
});
// socket.ioによるRapiroの制御
io.on('connection', function(socket) {
if(rapiroReady == false) { return; }
// console.log('socket ready');
socket.on('pose', function(obj) {
// console.log(obj);
posing(obj);
});
});
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Rapiro Posing Test</title>
<style>
td.center {text-align: center}
td.left {text-align: left}
td.right {text-align: right}
</style>
</head>
<body>
<table>
<!-- ポーズ名 -->
<tr>
<td class='left'>名前</td>
<td class='center'><input type='text' size='4' id='txtname' value='testPose'></td>
<td class='left'></td>
<td class='right'></td>
<td class='left'></td>
</tr>
<!-- ポーズ値 -->
<tr>
<td class='left'>頭</td>
<td class='center'><input type='text' size='4' id='txtheadYaw' value='90'></td>
<td class='right'>右</td>
<td class='center'><input type='range' id='rngheadYaw' min='0' max='180' value='90'></td>
<td class='left'>左</td>
</tr>
<tr>
<td class='left'>腰</td>
<td class='center'><input type='text' size='4' id='txtwaistYaw' value='90'></td>
<td class='right'>右</td>
<td class='center'><input type='range' id='rngwaistYaw' min='0' max='180' value='90'></td>
<td class='left'>左</td>
</tr>
<tr>
<td class='left'>右肩</td>
<td class='center'><input type='text' size='4' id='txtrightShoulderPitch' value='90'></td>
<td class='right'>下</td>
<td class='center'><input type='range' id='rngrightShoulderPitch' min='0' max='180' value='0'></td>
<td class='left'>上</td>
</tr>
<tr>
<td class='left'>右肩</td>
<td class='center'><input type='text' size='4' id='txtrightShoulderRoll' value='90'></td>
<td class='right'>開</td>
<td class='center'><input type='range' id='rngrightShoulderRoll' min='40' max='130' value='130'></td>
<td class='left'>閉</td>
</tr>
<tr>
<td class='left'>右手</td>
<td class='center'><input type='text' size='4' id='txtrightHandOpen' value='90'></td>
<td class='right'>閉</td>
<td class='center'><input type='range' id='rngrightHandOpen' min='70' max='120' value='90'></td>
<td class='left'>開</td>
</tr>
<tr>
<td class='left'>左肩</td>
<td class='center'><input type='text' size='4' id='txtleftShoulderPitch' value='90'></td>
<td class='right'>上</td>
<td class='center'><input type='range' id='rngleftShoulderPitch' min='0' max='180' value='180'></td>
<td class='left'>下</td>
</tr>
<tr>
<td class='left'>左肩</td>
<td class='center'><input type='text' size='4' id='txtleftShoulderRoll' value='90'></td>
<td class='right'>閉</td>
<td class='center'><input type='range' id='rngleftShoulderRoll' min='40' max='130' value='40'></td>
<td class='left'>開</td>
</tr>
<tr>
<td class='left'>左手</td>
<td class='center'><input type='text' size='4' id='txtleftHandOpen' value='90'></td>
<td class='right'>開</td>
<td class='center'><input type='range' id='rngleftHandOpen' min='70' max='120' value='90'></td>
<td class='left'>閉</td>
</tr>
<tr>
<td class='left'>右脚</td>
<td class='center'><input type='text' size='4' id='txtrightLegYaw' value='90'></td>
<td class='right'>内</td>
<td class='center'><input type='range' id='rngrightLegYaw' min='0' max='180' value='90'></td>
<td class='left'>外</td>
</tr>
<tr>
<td class='left'>右足</td>
<td class='center'><input type='text' size='4' id='txtrightFootRoll' value='90'></td>
<td class='right'>内</td>
<td class='center'><input type='range' id='rngrightFootRoll' min='0' max='180' value='90'></td>
<td class='left'>外</td>
</tr>
<tr>
<td class='left'>左脚</td>
<td class='center'><input type='text' size='4' id='txtleftLegYaw' value='90'></td>
<td class='right'>外</td>
<td class='center'><input type='range' id='rngleftLegYaw' min='0' max='180' value='90'></td>
<td class='left'>内</td>
</tr>
<tr>
<td class='left'>左足</td>
<td class='center'><input type='text' size='4' id='txtleftFootRoll' value='90'></td>
<td class='right'>外</td>
<td class='center'><input type='range' id='rngleftFootRoll' min='0' max='180' value='90'></td>
<td class='left'>内</td>
</tr>
<!-- LED値 -->
<tr>
<td class='left'>色R</td>
<td class='center'><input type='text' size='4' id='txtR' value='90'></td>
<td class='right'>暗</td>
<td class='center'><input type='range' id='rngR' min='0' max='255' value='127'></td>
<td class='left'>明</td>
</tr>
<tr>
<td class='left'>色G</td>
<td class='center'><input type='text' size='4' id='txtG' value='90'></td>
<td class='right'>暗</td>
<td class='center'><input type='range' id='rngG' min='0' max='255' value='127'></td>
<td class='left'>明</td>
</tr>
<tr>
<td class='left'>色B</td>
<td class='center'><input type='text' size='4' id='txtB' value='90'></td>
<td class='right'>暗</td>
<td class='center'><input type='range' id='rngB' min='0' max='255' value='127'></td>
<td class='left'>明</td>
</tr>
<!-- 動作速度 -->
<tr>
<td class='left'>速度</td>
<td class='center'><input type='text' size='4' id='txttimeInMs' value='90'></td>
<td class='right'>速</td>
<td class='center'><input type='range' id='rngtimeInMs' min='100' max='1000' value='500'></td>
<td class='left'>遅</td>
</tr>
</table>
リセットはページをリロードして→<input type='button' id='btnSend' value='リセット'>
<!-- 双方向通信(socket通信)のためのライブラリの読み込み(定型) -->
<script src='/socket.io/socket.io.js'></script>
<script>
// 双方向通信用のサーバに接続
var socket = io('http://192.168.**.***:3000'); // Raspberry Pi の IP
// 値の設定と、スライダーで値変更時の処理
var rngheadYaw = document.getElementById('rngheadYaw');
document.getElementById('txtheadYaw').value = rngheadYaw.value;
rngheadYaw.addEventListener('input', function() {
document.getElementById('txtheadYaw').value = rngheadYaw.value;
sendPose();
});
var rngwaistYaw = document.getElementById('rngwaistYaw');
document.getElementById('txtwaistYaw').value = rngwaistYaw.value;
rngwaistYaw.addEventListener('input', function() {
document.getElementById('txtwaistYaw').value = rngwaistYaw.value;
sendPose();
});
var rngrightShoulderPitch = document.getElementById('rngrightShoulderPitch');
document.getElementById('txtrightShoulderPitch').value = rngrightShoulderPitch.value;
rngrightShoulderPitch.addEventListener('input', function() {
document.getElementById('txtrightShoulderPitch').value = rngrightShoulderPitch.value;
sendPose();
});
var rngrightShoulderRoll = document.getElementById('rngrightShoulderRoll');
document.getElementById('txtrightShoulderRoll').value = rngrightShoulderRoll.value;
rngrightShoulderRoll.addEventListener('input', function() {
document.getElementById('txtrightShoulderRoll').value = rngrightShoulderRoll.value;
sendPose();
});
var rngrightHandOpen = document.getElementById('rngrightHandOpen');
document.getElementById('txtrightHandOpen').value = rngrightHandOpen.value;
rngrightHandOpen.addEventListener('input', function() {
document.getElementById('txtrightHandOpen').value = rngrightHandOpen.value;
sendPose();
});
var rngleftShoulderPitch = document.getElementById('rngleftShoulderPitch');
document.getElementById('txtleftShoulderPitch').value = rngleftShoulderPitch.value;
rngleftShoulderPitch.addEventListener('input', function() {
document.getElementById('txtleftShoulderPitch').value = rngleftShoulderPitch.value;
sendPose();
});
var rngleftShoulderRoll = document.getElementById('rngleftShoulderRoll');
document.getElementById('txtleftShoulderRoll').value = rngleftShoulderRoll.value;
rngleftShoulderRoll.addEventListener('input', function() {
document.getElementById('txtleftShoulderRoll').value = rngleftShoulderRoll.value;
sendPose();
});
var rngleftHandOpen = document.getElementById('rngleftHandOpen');
document.getElementById('txtleftHandOpen').value = rngleftHandOpen.value;
rngleftHandOpen.addEventListener('input', function() {
document.getElementById('txtleftHandOpen').value = rngleftHandOpen.value;
sendPose();
});
var rngrightLegYaw = document.getElementById('rngrightLegYaw');
document.getElementById('txtrightLegYaw').value = rngrightLegYaw.value;
rngrightLegYaw.addEventListener('input', function() {
document.getElementById('txtrightLegYaw').value = rngrightLegYaw.value;
sendPose();
});
var rngrightFootRoll = document.getElementById('rngrightFootRoll');
document.getElementById('txtrightFootRoll').value = rngrightFootRoll.value;
rngrightFootRoll.addEventListener('input', function() {
document.getElementById('txtrightFootRoll').value = rngrightFootRoll.value;
sendPose();
});
var rngleftLegYaw = document.getElementById('rngleftLegYaw');
document.getElementById('txtleftLegYaw').value = rngleftLegYaw.value;
rngleftLegYaw.addEventListener('input', function() {
document.getElementById('txtleftLegYaw').value = rngleftLegYaw.value;
sendPose();
});
var rngleftFootRoll = document.getElementById('rngleftFootRoll');
document.getElementById('txtleftFootRoll').value = rngleftFootRoll.value;
rngleftFootRoll.addEventListener('input', function() {
document.getElementById('txtleftFootRoll').value = rngleftFootRoll.value;
sendPose();
});
var rngR = document.getElementById('rngR');
document.getElementById('txtR').value = rngR.value;
rngR.addEventListener('input', function() {
document.getElementById('txtR').value = rngR.value;
sendPose();
});
var rngG = document.getElementById('rngG');
document.getElementById('txtG').value = rngG.value;
rngG.addEventListener('input', function() {
document.getElementById('txtG').value = rngG.value;
sendPose();
});
var rngB = document.getElementById('rngB');
document.getElementById('txtB').value = rngB.value;
rngB.addEventListener('input', function() {
document.getElementById('txtB').value = rngB.value;
sendPose();
});
var rngtimeInMs = document.getElementById('rngtimeInMs');
document.getElementById('txttimeInMs').value = rngtimeInMs.value;
rngtimeInMs.addEventListener('input', function() {
document.getElementById('txttimeInMs').value = rngtimeInMs.value;
sendPose();
});
function sendPose() {
// poseオブジェクト
var poseObj = {
name: document.getElementById('txtname').value,
pose: {
headYaw: parseInt(rngheadYaw.value),
waistYaw: parseInt(rngwaistYaw.value),
rightShoulderPitch: parseInt(rngrightShoulderPitch.value),
rightShoulderRoll: parseInt(rngrightShoulderRoll.value),
rightHandOpen: parseInt(rngrightHandOpen.value),
leftShoulderPitch: parseInt(rngleftShoulderPitch.value),
leftShoulderRoll: parseInt(rngleftShoulderRoll.value),
leftHandOpen: parseInt(rngleftHandOpen.value),
rightLegYaw: parseInt(rngrightLegYaw.value),
rightFootRoll: parseInt(rngrightFootRoll.value),
leftLegYaw: parseInt(rngleftLegYaw.value),
leftFootRoll: parseInt(rngleftFootRoll.value)
},
led: {
R: parseInt(rngR.value),
G: parseInt(rngG.value),
B: parseInt(rngB.value)
},
timeInMs: parseInt(rngtimeInMs.value)
};
console.log(poseObj);
socket.emit('pose', poseObj);
}
var btnSend = document.getElementById('btnSend');
btnSend.addEventListener('click', sendPose);
</script>
</body>
</html>
動作確認
- Rapiro の電源をON
- 上記二つのコードを Rapiro 内の Raspberry Pi の /home/pi/rapiro/pose_test/ に FTP する
- Rapiro 内の Paspberry Pi に SSH で接続し、node で app.js を実行
pi@raspberrypi:~/rapiro/pose_test $ node app.js
- まずは前回同様、直立状態になることを確認
- PC や スマホのブラウザで、Raspberry Pi のIPアドレスにアクセス
http://192.168.**.***:3000
- 以下のような操作インタフェース(分かりにくい…)
- スライダーをグリグリ動かして所望のポーズにして遊ぶ
- スライダーをグリグリ動かしてLEDの色も変えて遊ぶ
- わからなくなったら、リロードして[リセット]を押す
- いろいろなブラウザで試してみる
- PC の Chrome, Firefox で OK でした
- iOS の Safari, Chrome, Firefox で OK でした
解説
今日は疲れたので、後日、気が向いたら書きます。
汚らしいコードでたいへんお恥ずかしいです。
まとめ
実は今回は変なところでつまずきました。html 側の JavaScript で、いつもは const や let の宣言で問題なく動くのに、なぜか今回は動きません。しかも、PC のブラウザでは問題ないのに、スマホだけ、どのブラウザでもダメでした。やむを得ず全て var にしました。まさかそんな原因とは考えが及ばず、数時間ロスしました。
しかしながら、動いた時は大いに感動しました。これで Rapiro の全てをコントロールする基礎ができました。
次回 [3] では、歩行をはじめとして、様々な連続的な動作(ポーズを次々に変えて動く)のプログラムに移りたい考えです。少なくとも、Rapiro 標準ファームウェアに備わっている10個の動作は再現ーしたいと思います。
最新コードは以下にあります。
https://github.com/mkokubun/rapiro.js