やりたいこと
最終目標(前回と同じ)
- 二足歩行ロボット Rapiro の制御を、標準の Arduino IDE(C/C++)ベースから、JavaScript(Node.js)ベースに移植し、Rapiro を IoT デバイスっぽくする
- ([1]で完了)
かつ、PCでの制御ではなく、Rapiro 内部に搭載した Raspberry Pi での制御とし、完全無線化を図る - 全機能の移植が難しくとも、最低限、10個の基本動作は移植・再現する
- 可能な限り、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容も移植する
- 距離センサの搭載
- 静電容量タッチセンサの搭載など
- この Rapiro をベースに、さらに賢そうな遊び方を模索する
今回 [3] の目標
- Rapiro の標準ファームウェアに実装された10個の基本動作を全て移植・再現する
- 完全に正確な再現ではないが…
方法
機材(前回とほぼ同じ)
- Rapiro
- 電源を入れる前に、Arduino IDE を用いて、制御ボードに StandardFirmata を書き込んでおく
- いつでも Rapiro の標準ファームウェアに戻せるので問題なし
- Raspberry Pi 3 Model B
- Rapiro 内部に搭載済(こちらの手順で)
- Node.js インストール済(こちらの手順で)
- エネループ(単3)5本 または Rapiro 用 ACアダプタ
- PC(Windows10、Raspberry Pi に SSH や FTP できればなんでも良い)
- スマホまたはタブレット
- 無線LAN環境
手続き
準備
- Rapiro 内の Raspberry Pi に SSH でアクセスし、npm で johnny-five、socket.io、express をインストール
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の設定や動作等を格納するオブジェクト
// メインの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 によるブラウザからの制御と、現在状態をブラウザに送る処理を書きました。
// 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 側から返される現在の動作名と表情名が表示されます。
<!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> </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> </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動作を実装することができました。
無線制御といっても Bluetooth ではありませんので、やりようによっては地球の裏側からでも Rapiro を制御できるはずです(現状はLAN内限定です)。
なお、johnny-five の サーボアニメーションの機能を使えばわりと簡単かと思って作り始めましたが、結局、さほど効率的なコードにはならなかったと思います。
いずれにせよ、これで最終目標の 1 は達成できました。次回以降、可能な限り、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容(例えば以下)も移植していきたい所存です。
- 距離センサの搭載
- 静電容量タッチセンサの搭載など
最新コードは以下にあります。
https://github.com/mkokubun/rapiro.js