4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

二足歩行ロボット Rapiro を Node.js で制御 [3] 10個の基本動作を実装

Last updated at Posted at 2017-01-16

やりたいこと

最終目標(前回と同じ)

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

今回 [3] の目標

方法

機材(前回とほぼ同じ)

  • Rapiro
  • 電源を入れる前に、Arduino IDE を用いて、制御ボードに StandardFirmata を書き込んでおく
  • いつでも Rapiro の標準ファームウェアに戻せるので問題なし
  • Raspberry Pi 3 Model B
  • Rapiro 内部に搭載済(こちらの手順で)
  • Node.js インストール済(こちらの手順で)
  • エネループ(単3)5本 または Rapiro 用 ACアダプタ
  • PC(Windows10、Raspberry Pi に SSH や FTP できればなんでも良い)
  • スマホまたはタブレット
  • 無線LAN環境

手続き

準備

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

コーディング

  • 以下の三つのコードを入力し、同じ階層(e.g. /home/pi/rapiro/)に置く
  • rapiro-cfg.js : Rapiro の各種設定を格納するオブジェクト
  • app.js : 制御プログラム本体
  • index.html : 操作インタフェース
  • 実際には、PC の エディタ(Microsoft Visual Studio Code)で書いて、Raspberry Pi に FTP しました

Rapiro の各種設定を格納するオブジェクト

以下の4つのオブジェクトを設定しました。制御プログラム本体(app.js)内に書くと見にくくなるので、別ファイルに分けて、app.jsからは require() で読み込むようにしました。

  • servo : 12個の各サーボのピン番号、開始角度、可動域、オフセット
  • motion : 10個の標準動作のアニメーションの設定
  • led : 3色のLEDのピン番号
  • face : LEDのアニメーションの設定
rapiro-cfg.js
// Rapiroの設定や動作等を格納するオブジェクト
// メインのjs内で例えば cfg = require('./rapiro-cfg'); のように参照
// Rapiro標準の10モーション実装
// 2017.01.17 by mkoku

'use strict';

// サーボの諸設定
// servoという名前で外部から読めるようにexports
exports.servo = {
    head: {     // 0. head
        pin:     10,        // ピン番号
        startAt: 90,        // 開始角度
        range:   [0, 180],  // 可動域
        offset:  -7         // オフセット(トリム)
    },
    waist: {    // 1. waist
        pin:     11,
        startAt: 90,
        range:   [0, 180],
        offset:  2
    },
    r_s_r: {    // 2. right shoulder roll [up / down]
        pin:     9,
        startAt: 0,
        range:   [0, 180],
        offset:  0
    },
    r_s_p: {    // 3. right shoulder pitch [open / close]
        pin:     8,
        startAt: 130,
        range:   [40, 130],
        offset:  0
    },
    r_h_g: {    // 4. right hand grip
        pin:     7,
        startAt: 90,
        range:   [50, 110],
        offset:  0
    },
    l_s_r: {    // 5. left shoulder roll [up / down]
        pin:     12,
        startAt: 180,
        range:   [0, 180],
        offset:  0
    },
    l_s_p: {    // 6. left shoulder pitch [open /close]
        pin:     13,
        startAt: 50,
        range:   [40, 130],
        offset:  0
    },
    l_h_g: {    // 7. left hand grip
        pin:     14,
        startAt: 90,
        range:   [70, 130],
        offset:  0
    },
    r_f_y: {    // 8. right foot yaw
        pin:     4,
        startAt: 90,
        range:   [45, 135],
        offset:  -10
    },
    r_f_p: {    // 9. right foot pitch
        pin:     2,
        startAt: 90,
        range:   [40, 125],
        offset:  6
    },
    l_f_y: {    // 10. left foot yaw
        pin:     15,
        startAt: 90,
        range:   [45, 135],
        offset:  9
    },
    l_f_p: {    // 11. left foot pitch
        pin:     16,
        startAt: 90,
        range:   [55, 140],
        offset:  -12
    }
};

// モーション(動作アニメーション)の諸設定
// motionという名前で外部から読めるようにexports
exports.motion = {
    // 0. stop
    stop: {
        duration: 1000,         // 再生時間[ms]
        loop: false,            // ループ再生有無
        cuePoints: [0.0, 1.0],  // キューポイント
        keyFrames: [            // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: this.servo.head.startAt,  easing:'inOutSine'}], // 頭
            [null,  {value: this.servo.waist.startAt, easing:'inOutSine'}], // 腰
            [null,  {value: this.servo.r_s_r.startAt, easing:'inOutSine'}], // 右肩ロール(上下)
            [null,  {value: this.servo.r_s_p.startAt, easing:'inOutSine'}], // 右肩ピッチ(開閉)
            [null,  {value: this.servo.r_h_g.startAt, easing:'inOutSine'}], // 右手
            [null,  {value: this.servo.l_s_r.startAt, easing:'inOutSine'}], // 左肩ロール(上下)
            [null,  {value: this.servo.l_s_p.startAt, easing:'inOutSine'}], // 左肩ピッチ(開閉)
            [null,  {value: this.servo.l_h_g.startAt, easing:'inOutSine'}], // 左手
            [null,  {value: this.servo.r_f_y.startAt, easing:'inOutSine'}], // 右足ヨー(開閉)
            [null,  {value: this.servo.r_f_p.startAt, easing:'inOutSine'}], // 右足ピッチ(内外)
            [null,  {value: this.servo.l_f_y.startAt, easing:'inOutSine'}], // 左足ヨー(開閉)
            [null,  {value: this.servo.l_f_p.startAt, easing:'inOutSine'}]  // 左足ピッチ(内外)
        ]
    },
    // 1. forward
    forward: {
        duration: 3000,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.25, 0.5, 0.75, 1.0],     // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 80, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value:100, easing:'inOutSine'}, {value:110, easing:'inOutSine'}],
            [null,  {value:110, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 80, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value:100, easing:'inOutSine'}, {value:110, easing:'inOutSine'}],
            [null,  {value:120, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}]
        ]
    },
    // 2. back
    back: {
        duration: 3000,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.25, 0.5, 0.75, 1.0],     // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:100, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value: 80, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}],
            [null,  {value:110, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 30, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:100, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value: 80, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}],
            [null,  {value:120, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}]
        ]
    },
    // 3. right
    right: {
        duration: 3000,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.25, 0.5, 0.75, 1.0],     // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 95, easing:'inOutSine'}, {value:100, easing:'inOutSine'}, {value: 85, easing:'inOutSine'}, {value: 80, easing:'inOutSine'}],
            [null,  {value: 60, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value:110, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 85, easing:'inOutSine'}, {value: 80, easing:'inOutSine'}, {value: 95, easing:'inOutSine'}, {value:100, easing:'inOutSine'}],
            [null,  {value: 70, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}]
        ]
    },
    // 4. left
    left: {
        duration: 3000,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.25, 0.5, 0.75, 1.0],     // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 95, easing:'inOutSine'}, {value:100, easing:'inOutSine'}, {value: 85, easing:'inOutSine'}, {value: 80, easing:'inOutSine'}],
            [null,  {value:110, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 85, easing:'inOutSine'}, {value: 80, easing:'inOutSine'}, {value: 95, easing:'inOutSine'}, {value:100, easing:'inOutSine'}],
            [null,  {value:120, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}]
        ]
    },
    // 5. green
    green: {
        duration: 1500,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.4, 0.6, 0.8, 1.0],       // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value:100, easing:'inOutSine'}, {value: 80, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:120, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value:120, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value:130, easing:'inOutSine'}, {value:130, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value:110, easing:'inOutSine'}, {value:110, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 60, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}]
        ]
    },
    // 6. yellow
    yellow: {
        duration: 1500,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.5, 1.0],                 // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:120, easing:'inOutSine'}, {value:120, easing:'inOutSine'}],
            [null,  {value:120, easing:'inOutSine'}, {value:120, easing:'inOutSine'}],
            [null,  {value:130, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}]
        ]
    },
    // 7. blue
    blue: {
        duration: 5000,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.2, 0.3, 0.4, 0.5, 0.7, 0.8, 1.0],     // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:120, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value:120, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value:130, easing:'inOutSine'}, {value:130, easing:'inOutSine'}, {value:130, easing:'inOutSine'}, {value:130, easing:'inOutSine'}, {value:130, easing:'inOutSine'}, {value:130, easing:'inOutSine'}, {value:130, easing:'inOutSine'}],
            [null,  {value: 70, easing:'inOutSine'}, {value:110, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value:110, easing:'inOutSine'}, {value:110, easing:'inOutSine'}, {value:110, easing:'inOutSine'}, {value:110, easing:'inOutSine'}],
            [null,  {value: 60, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 50, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}],
            [null,  {value:110, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value:110, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}]
        ]
    },
    // 8. red
    red: {
        duration: 1500,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.5, 1.0],                 // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 60, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}],
            [null,  {value:  0, easing:'inOutSine'}, {value:  0, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 60, easing:'inOutSine'}, {value: 60, easing:'inOutSine'}],
            [null,  {value: 50, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}]
        ]
    },
    // 9. push
    push: {
        duration: 5000,                             // 再生時間[ms]
        loop: true,                                 // ループ再生有無
        cuePoints: [0.0, 0.2, 0.4, 0.6, 1.0],       // キューポイント
        keyFrames: [                                // キーフレーム(行:サーボ12個 × 列:キューポイントごとの角度とイージングの種類)
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 40, easing:'inOutSine'}, null],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value:140, easing:'inOutSine'}, null],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, null],
            [null,  {value:130, easing:'inOutSine'}, {value:130, easing:'inOutSine'}, {value: 70, easing:'inOutSine'}, null],
            [null,  {value:110, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, null],
            [null,  {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, {value:180, easing:'inOutSine'}, null],
            [null,  {value: 50, easing:'inOutSine'}, {value: 50, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, null],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, null],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, null],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, null],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, null],
            [null,  {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, {value: 90, easing:'inOutSine'}, null]
        ]
    }
};

// LEDの諸設定
// ledという名前で外部から読めるようにexports
exports.led = { R: {pin: 6}, G: {pin: 5}, B: {pin: 3} };

// 表情の諸設定
// faceという名前で外部から読めるようにexports
exports.face = {
    white: {
        duration: 3000,                         // 再生時間
        loop: true,                             // ループ再生有無
        cuePoints: [0.0, 0.4, 0.5, 0.9, 1.0],   // キューポイント
        keyFrames: [                            // キーフレーム(行:LED3個 × 列:キューポイントごとの明るさとイージングの種類)
            [{value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}]
        ]
    },
    red: {
        duration: 1500,
        loop: true,
        cuePoints: [0.0, 1.0],
        keyFrames: [
            [{value:0, easing:'linear'}, {value:255, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:  0, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:  0, easing:'linear'}]
        ]
    },
    green: {
        duration: 2000,
        loop: true,
        cuePoints: [0.0, 0.25, 0.5, 0.75, 1.0],
        keyFrames: [
            [{value:0, easing:'linear'}, {value:  0, easing:'linear'}, {value:0, easing:'linear'}, {value:  0, easing:'linear'}, {value:0, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:  0, easing:'linear'}, {value:0, easing:'linear'}, {value:  0, easing:'linear'}, {value:0, easing:'linear'}]
        ]
    },
    blue: {
        duration: 3000,
        loop: true,
        cuePoints: [0.0, 0.4, 0.5, 0.9, 1.0],
        keyFrames: [
            [{value:0, easing:'linear'}, {value:  0, easing:'linear'}, {value:0, easing:'linear'}, {value:  0, easing:'linear'}, {value:0, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:  0, easing:'linear'}, {value:0, easing:'linear'}, {value:  0, easing:'linear'}, {value:0, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}, {value:255, easing:'linear'}, {value:0, easing:'linear'}]
        ]
    },
    yellow: {
        duration: 1500,
        loop: true,
        cuePoints: [0.0, 1.0],
        keyFrames: [
            [{value:0, easing:'linear'}, {value:255, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:255, easing:'linear'}],
            [{value:0, easing:'linear'}, {value:  0, easing:'linear'}]
        ]
    },
};

制御プログラム本体

基本部分は johnny-five のサーボアニメーション を参考に書きました。
加えて、socket.io によるブラウザからの制御と、現在状態をブラウザに送る処理を書きました。

app.js
// Rapiro を 内蔵 Raspberry Pi から Node.js + johnny-five で制御
// Rapiro標準の10モーション実装
// socket.ioによりブラウザから制御
// 2017.01.17 by mkoku

'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 rapiro = {                                // Rapiroの設定や動作等を格納するオブジェクト
    ready: false                                // Rapiroの準備状態(初期値false)
};


// 制御ボードの準備ができたら
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')");

    // 終了時の処理
    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で送る
            socket.emit('response', {
                motion: rapiro.currentMotionName,   // 動作名
                face:   rapiro.currentFaceName      // 表情名
            });
        }
    });
});

操作インタフェース

10個の基本動作用のボタンを配置して、ボタンに応じて動作(サーボのアニメーション)の名前と、表情(LEDのアニメーション)の名前を、socket で Rapiro に送っています。
また、Rapiro 側から返される現在の動作名と表情名が表示されます。

index.html
<!DOCTYPE html>
<!--
// Rapiro制御インタフェース
// Rapiro標準の10モーション実装
// 2017.01.17 by mkoku
-->
<html>
<head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>
    <title>Rapiro Walking Test</title>
    <style>
        td.center {text-align: center}
    </style>
</head>

<body>
    <table>
        <!-- Rapiroからのレスポンスを表示する場所 -->
        <tr>
            <td class='center' colspan='3' id='msg'>motion/face</td>
        </tr>
        <tr><td>&nbsp;</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>&nbsp;</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;  // 動作と表情を表示
        });

    </script>
</body>
</html>

動作確認

  • 上記3つのコードを Rapiro 内部の Raspberry Pi に FTP
  • e.g. /home/pi/rapiro/ 内に
  • SSH で Rapiro 内部の Raspberry Pi にアクセスし、node で app.js を実行
  • 今回はこの SSH もスマホから試してみました(後述の動画をご参考)
  • 準備ができると Rapiro のサーボに通電され直立状態となり、目は白色にフェードする
pi@raspberrypi:~/rapiro $ node app.js

ブラウザから

  • PC や スマホのブラウザで、Raspberry Pi のIPアドレスにアクセス
http://192.168.**.***:3000
  • ブラウザ画面の10個のボタンで Rapiro を動かして遊ぶ

SSH のコマンドラインから

  • rapiro.execMotion('forward') や rapiro.execFace('blue') などのコマンドをうつ

  • rapiro.execMotion('動作名')

  • rapiro.execFace('表情名')

pi@raspberrypi:~/rapiro $ node app.js
1484623993712 Connected /dev/ttyAMA0
1484623998794 Repl Initialized
>> Type rapiro.execMotion('name') or rapiro.execFace('name')
>> rapiro.execMotion('forward')
undefined

まとめ

以下の動画のようにできました。それぞれの動作は完全なコピーではありませんが、概ね、標準の10動作を実装することができました。

Rapiro を node.js (johnny-five + socket.io) で制御

無線制御といっても Bluetooth ではありませんので、やりようによっては地球の裏側からでも Rapiro を制御できるはずです(現状はLAN内限定です)。

なお、johnny-five の サーボアニメーションの機能を使えばわりと簡単かと思って作り始めましたが、結局、さほど効率的なコードにはならなかったと思います。

いずれにせよ、これで最終目標の 1 は達成できました。次回以降、可能な限り、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容(例えば以下)も移植していきたい所存です。

  • 距離センサの搭載
  • 静電容量タッチセンサの搭載など

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

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?