1. Qiita
  2. 投稿
  3. RAPIRO

二足歩行ロボット Rapiro を Node.js で制御 [2] ブラウザからポージング

  • 1
    いいね
  • 0
    コメント

やりたいこと

最終目標(前回と同じ)

  1. 二足歩行ロボット Rapiro の制御を、標準の Arduino IDE(C/C++)ベースから、JavaScript(Node.js)ベースに移植し、Rapiro を IoT デバイスっぽくする
    • かつ、PCでの制御ではなく、Rapiro 内部に搭載した Raspberry Pi での制御とし、完全無線化を図る
    • 全機能の移植が難しくとも、最低限、9個の基本動作は移植・再現する
  2. 可能な限り、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容も移植する
    • 距離センサーの搭載
    • 静電容量タッチセンサの搭載など
  3. この Rapiro をベースに、さらに賢そうな遊び方を模索する

今回 [2] の目標

  • 全てのサーボとLEDを自由に制御する基礎を作る
    • ポージングさせるだけ
    • 歩行のような連続動作は行わない
  • ブラウザから操作する
    • LAN内限定だが、スマホからでも操作
    • Rapiroの電源以外は無線
    • IoTっぽい

方法

機材(前回とほぼ同じ)

  • Rapiro
  • Raspberry Pi 3 Model B
    • Rapiro 内部に搭載済
    • Node.js インストール済
  • Rapiro 用 ACアダプタ
  • PC(Windows10、Raspberry Pi に SSH や FTP できればなんでも良い)
  • スマホ
  • 無線LAN 環境

手続き

準備

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 する
  • ターミナルソフトウェアを使って PC から SSH で接続する
  • node で app.js を実行
pi@raspberrypi:~/rapiro/pose_test $ node app.js
  • まずは前回同様、直立状態になることを確認
  • PC や スマホのブラウザで、Raspberry Pi のIPアドレスにアクセス
http://192.168.**.***:3000
  • 以下のような操作インタフェース(分かりにくい…)
    • スライダーをグリグリ動かして所望のポーズにして遊ぶ
    • スライダーをグリグリ動かしてLEDの色も変えて遊ぶ
    • わからなくなったら、リロードして[リセット]を押す

capture.JPG

  • いろいろなブラウザで試してみる
    • PC の Chrome, Firefox で OK でした
    • iOS の Safari, Chrome, Firefox で OK でした

解説

今日は疲れたので、後日、気が向いたら書きます。
汚らしいコードでたいへんお恥ずかしいです。

所感と今後

実は今回は変なところでつまずきました。
html 側の JavaScript で、いつもは const や let の宣言で問題なく動くのに、なぜか今回は動きません。PC のブラウザでは問題ないのに、スマホだけ、どのブラウザでもダメでした。やむを得ず全て var にしました。まさかのそんな原因になかなかたどり着けず、数時間ロスしました。
しかしながら、動いた時は大いに感動しました。これで Rapiro の全てをコントロールする基礎ができました。
次回 [3] では、歩行をはじめとして、様々な連続的な動作(ポーズを次々に変えて動く)のプログラムに移りたい考えです。少なくとも、Rapiro 標準ファームウェアに備わっている9個の動作は再現ーしたいと思います。

Comments Loading...