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

# やりたいこと

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

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


# 方法

## 機材（[前回](http://qiita.com/mkoku/items/191ead1b62693003bf64)とほぼ同じ）
* [Rapiro](http://www.rapiro.com/ja/)
 * 電源を入れる前に、Arduino IDE を用いて、制御ボードに StandardFirmata を書き込んでおく
 * いつでも [Rapiro の標準ファームウェア](https://github.com/Ishiwatari/RAPIRO)に戻せるので問題なし
* Raspberry Pi 3 Model B
 * Rapiro 内部に搭載済（[こちらの手順](http://qiita.com/mkoku/items/191ead1b62693003bf64)で）
 * Node.js インストール済（[こちらの手順](http://qiita.com/mkoku/items/111e6ec21395065f0c28)で）
* [Rapiro 用 ACアダプタ](https://www.switch-science.com/catalog/1940/)
* PC（Windows10、Raspberry Pi に SSH や FTP できればなんでも良い）
* スマホまたはタブレット
* 無線LAN環境

## 手続き

### 準備
* [Rapiro の標準ファームウェア](https://github.com/Ishiwatari/RAPIRO)（Arduino のスケッチ）や[書籍「二足歩行ロボット 工作＆プログラミング（リックテレコム）」](http://www.ric.co.jp/book/contents/book_995.html)を参考に、Rapiro の制御アルゴリズムを勉強する
* Rapiro 内の Raspberry Pi に SSH でアクセスし、npm で [johnny-five](http://johnny-five.io/)、[socket.io](http://socket.io/)、[express](http://expressjs.com/ja/) をインストール

```
pi@raspberrypi:~ $ npm install johnny-five socket.io express
```

### コーディング
* 制御プログラム本体（app.js）と、操作インタフェース（index.html）を以下のように書く（ものすごい見にくいコードだが、とりあえず動いたので良し）
 * 例として、/home/pi/rapiro/pose_test 内に作成
 * 実際には、PC の エディタ（[Microsoft Visual Studio Code](https://code.visualstudio.com/)）で書いて、Raspberry Pi に FTP しました

```js: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);
    });
});
```

```html: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
```

* まずは[前回]((http://qiita.com/mkoku/items/191ead1b62693003bf64))同様、直立状態になることを確認
* PC や スマホのブラウザで、Raspberry Pi のIPアドレスにアクセス

```
http://192.168.**.***:3000
```

* 以下のような操作インタフェース（分かりにくい…）
 * スライダーをグリグリ動かして所望のポーズにして遊ぶ
 * スライダーをグリグリ動かしてLEDの色も変えて遊ぶ
 * わからなくなったら、リロードして[リセット]を押す

![capture.JPG](https://qiita-image-store.s3.amazonaws.com/0/157570/1c3c3036-23eb-e4cb-c673-1c1745020c72.jpeg)

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

[![二足歩行ロボット Rapiro を Node.js で制御 － ブラウザからポージング ](http://img.youtube.com/vi/hnc8a8GeCxE/0.jpg)](https://youtu.be/hnc8a8GeCxE)

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


# まとめ

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

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