LoginSignup
5
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-01-09

やりたいこと

最終目標(前回と同じ)

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

今回 [2] の目標

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

方法

機材(前回とほぼ同じ)

手続き

準備

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の色も変えて遊ぶ
    • わからなくなったら、リロードして[リセット]を押す

capture.JPG

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

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

解説

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

まとめ

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

最新コードは以下にあります。
https://github.com/mkokubun/rapiro.js

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1