Edited at

二足歩行ロボット Rapiro を Node.js で制御 [6] 音声合成と天気予報

More than 1 year has passed since last update.


やりたいこと


最終目標(前回から引き継ぎ)


  1. [3]で完了)二足歩行ロボット Rapiro の制御を、標準の Arduino IDE(C/C++)ベースから、JavaScript(Node.js)ベースに移植し、Rapiro を IoT デバイスっぽくする


    • [1][2]で完了)かつ、PCでの制御ではなく、Rapiro 内部に搭載した Raspberry Pi での制御とし、完全無線化を図る

    • [3]で完了)全機能の移植が難しくとも、最低限、10個の基本動作は移植・再現する



  2. [5]で完了)可能な限り、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容も移植する


    • [4]で完了)距離センサの搭載

    • [5]で完了)静電容量タッチセンサの搭載など



  3. この Rapiro をベースに、さらに賢そうな遊び方を模索する


    • スピーカーの搭載、音声合成

    • 天気予報

    • 赤外線による家電の操作

    • カメラの搭載、ストリーミング

    • 音声認識

    • 人工知能?による簡単な会話

    • etc.




今回 [6] の目標


  • 多くの Rapiro ファンが実施していらっしゃる、音声合成と天気予報に挑戦



  • そのために Rapiro にスピーカーを搭載


方法


機材



  • Rapiro


    • 電源を入れる前に、こちらの手順で Arduino IDE を用いて、制御ボードに StandardFirmataPlus を書き込んでおく



  • Raspberry Pi 3 Model B



  • エネループ(単3)5本 または Rapiro 用 ACアダプタ

  • PC(Windows10、Raspberry Pi に SSH や FTP できればなんでも良い)

  • スマホまたはタブレット

  • 無線LAN環境


  • [4]から距離センサは搭載済


  • [5]からタッチセンサは搭載済


スピーカーの搭載

IMG_0119.JPG


コーディング


概要


  • 以下の三つのコードを入力し、同じ階層(e.g. /home/pi/rapiro/)に置く



    • rapiro-cfg.js : Rapiro の各種設定を格納するオブジェクト(前回と同一)


    • app.js : 制御プログラム本体


    • index.html : 操作インタフェース


      • 今回は app.js と index.html のみ前回から後述のように改編





  • 動作の概要


    • 起動音が鳴る

    • 各 Feeler に触れるとタッチ音が鳴る

    • Feeler #0(右の角)に触れると天気情報を話す


      • この例では名古屋市の翌日・翌々日の天気と気温



    • Feeler #1(右の耳)に触れると現在時刻を話す

    • htmlからも時刻と天気予報を話す指示が出せる




制御プログラム本体(app.js)

前回からの主な改編部分は以下


  • 45~56行目: VoiceText による音声合成、音声ファイルの再生、OpenWeatherMap に関する設定

  • 145行目:起動音の再生

  • 166行目:距離センサの値が一定未満の場合の警告音の再生

  • 202, 205行目:タッチセンサに触れることで天気と時刻を話す

  • 216行目:タッチセンサに触れた時に効果音を再生

  • 255~261行目:htmlからの時刻と天気を話すsocketの処理

  • 299~441行目:音声合成と天気予報のための関数群


app.js

// 音声合成と天気予報

// VoiceText / OpenWeatherMap Web API 利用
//  ・起動音・障害物検出音・タッチセンサタッチ音、および、時刻・天気の音声合成
//  ・[04]距離センサと[05]静電容量タッチセンサ搭載前提
//  ・事前に VoiceText と OpenWeatherMap の API key を取得しておく
// https://cloud.voicetext.jp/webapi
// http://openweathermap.org/
//  ・モジュール voicetext と openweather-apis をインストールしておく
// npm install voicetext
// npm install openweather-apis
// 2017.01.29 by Mitsuteru Kokubun

'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 obstacleThreshold = 9; // 障害物検出と判定する距離センサの閾値[cm]
const MPR121ADDR = 0x5A; // MPR121静電容量タッチセンサコントローラのアドレス
const feelerNum = 4; // タッチセンサのfeelerの数
const rapiro = { // Rapiroの設定や動作等を格納するオブジェクト
ready: false, // Rapiroの準備状態(初期値false)
power: 0, // サーボ電源の状態(0:OFF / 1:ON)
obstacle: false, // 障害物の検出状態(初期値false)
touch: [0, 0, 0, 0], // タッチセンサのタッチ状態(初期値4ch全て0)
touched: [0, 0, 0, 0]
};

// 音声合成(VoiceText)の設定
const VoiceText = require('voicetext'); // voicetextモジュールを使う
const voice = new VoiceText('xxxxxxxxxxxxxxxx'); // VoiceTextのインスタンス(API key)

// 音声ファイル再生のための設定
const exec = require('child_process').exec; // シェルコマンド実行用の子プロセス
const fs = require('fs'); // ファイル入出力

// 天気情報(OpenWeatherMap)の設定
const weather = require('openweather-apis'); // openweather-apisモジュールを使う
const city = 1856057; // 都市ID(http://openweathermap.org/help/city_list.txt)
weather.setAPPID('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); // OpenWeatherMap の API key

// 制御ボードの準備ができたら
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) { // 指定の動作名のプロパティがあったら
if (rapiro.power == 0) {
rapiro.powerSwitch(1);
}
rapiro.currentMotionName = pname; // 現在の動作名をその動作名に設定
rapiro.currentMotionSequence = obj[pname]; // 現在の動作シーケンスにその動作を格納
if (rapiro.currentMotionSequence.loop == false) { // 非ループ動作なら終了時に電源OFF(省電力)
rapiro.currentMotionSequence.oncomplete = function () {
rapiro.powerSwitch(0);
}
}
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/OFFする関数(引数 0:OFF / 1:ON)
rapiro.powerSwitch = function(OnOff) {
board.pinMode(pinServoDC, five.Pin.OUTPUT); // 電源供給ピンを出力モードに
board.digitalWrite(pinServoDC, OnOff); // 電源供給ピンに1を出力
rapiro.power = OnOff; // 電源状態の変数を変更
}

// 初期状態を作る
rapiro.powerSwitch(1); // サーボ電源をON
rapiro.execMotion('stop'); // 動作をstopに
rapiro.execFace('white'); // 表情をwhiteに
rapiro.ready = true; // Rapiroの準備OK
rapiro.playSound('decision24.wav'); // 06起動音の再生

// デバッグ用:コマンドラインからモーションと表情を制御
board.repl.inject({
rapiro: rapiro
});
console.log("Type rapiro.execMotion('name') or rapiro.execFace('name')");

// 距離センサ(GP2Y0A21YK)の処理
const proximity = new five.Proximity({
controller: 'GP2Y0A21YK', // コントローラに GP2Y0A21YK を指定
pin: 'A6', // A6 ピンを指定
freq: 250 // サンプリング間隔[ms]
});
// 距離センサのデータが取得されたら
proximity.on('data', function() {
const dist = this.cm; // cm or in(inch)
// 障害物の処理
if ((dist < obstacleThreshold) && (rapiro.obstacle == false)) {
// 距離が閾値未満かつ現在障害物が未検出状態だったら
rapiro.obstacle = true; // 障害物検出状態に
rapiro.playSound('warning1.wav'); // 06警告音の再生
rapiro.previousMotionName = rapiro.currentMotionName; // 現在の動作名を一時保管
rapiro.previousFaceName = rapiro.currentFaceName; // 現在の表情名を一時補間
rapiro.execMotion('blue'); // 動作blueを実行
rapiro.execFace('red'); // 表情redを実行
} else if ((dist >= obstacleThreshold) && (rapiro.obstacle == true)) {
// 距離が閾値以上かつ現在障害物が検出状態だったら
rapiro.obstacle = false; // 障害物未検出状態に
rapiro.execMotion(rapiro.previousMotionName); // 前の動作を再開
rapiro.execFace(rapiro.previousFaceName); // 前の表情を再開
}
// 距離の値と現状の動作・表情をsocketで送る
emitSocket('proximity', {
distance: dist // 距離
});
emitSocket('response', {
motion: rapiro.currentMotionName, // 動作名
face: rapiro.currentFaceName // 表情名
});
});

// タッチセンサ(Grove I2C MPR121)の処理
this.i2cConfig(); // I2Cを使うためのjohnny-fiveのおまじない
mpr121Config(this); // MPR121静電容量タッチセンサコントローラを初期設定
// タッチセンサの状態の読み込みとタッチの判定(i2cRead()は繰り返し実行される)
this.i2cRead(MPR121ADDR, 1, function(bytes){
(bytes & 0x01) == 0x01 ? rapiro.touch[0] = 1 : rapiro.touch[0] = 0; // ch0を判定
(bytes & 0x02) == 0x02 ? rapiro.touch[1] = 1 : rapiro.touch[1] = 0; // ch1を判定
(bytes & 0x04) == 0x04 ? rapiro.touch[2] = 1 : rapiro.touch[2] = 0; // ch2を判定
(bytes & 0x08) == 0x08 ? rapiro.touch[3] = 1 : rapiro.touch[3] = 0; // ch3を判定
for (let t = 0; t < feelerNum; t++) {
if ((rapiro.touch[t] == 1) && (rapiro.touched[t] == 0)) {
// タッチセンサに触れた時(触れていなかった状態から)
rapiro.touched[t] = 1;
switch (t) {
case 0:
rapiro.talkWeather(city, 1, 2); // 06天気を話す(都市ID, 0当日、1翌日... ~4)
break;
case 1:
rapiro.talkTime(); // 06時刻を話す
break;
case 2:
rapiro.execMotion('stop'); // 動作stopを実行
rapiro.execFace('white'); // 表情whiteを実行
break;
case 3:
rapiro.execMotion('red'); // 動作red実行
rapiro.execFace('red'); // 表情redを実行
break;
}
rapiro.playSound('decision22.wav'); // 06タッチ音の再生
}
if ((rapiro.touch[t] == 0) && (rapiro.touched[t] == 1)) {
// タッチセンサから離した時(触れた状態から)
rapiro.touched[t] = 0;
}
}
// タッチセンサの値と現状の動作・表情をsocketで送る
emitSocket('touch', {
touch: rapiro.touch // タッチセンサの状態
});
emitSocket('response', {
motion: rapiro.currentMotionName, // 動作名
face: rapiro.currentFaceName // 表情名
});
});

// 終了時の処理
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で送る
emitSocket('response', {
motion: rapiro.currentMotionName, // 動作名
face: rapiro.currentFaceName // 表情名
});
}
});
socket.on('time', function(data) { // 06時刻を話す
rapiro.talkTime();
rapiro.playSound('decision22.wav'); // タッチ音の再生
});
socket.on('weather', function(data) { // 06天気を話す
rapiro.talkWeather(city, 1, 1); // 翌日の天気のみ
rapiro.playSound('decision22.wav'); // タッチ音の再生
});
});

// ブラウザにsocketでデータを送る
function emitSocket(event, data) {
if (socket != null) {
socket.emit(event, data);
}
}

// MPR121静電容量タッチセンサコントローラの初期化関数
function mpr121Config(device) {
// Section A: dataがベースラインより大きい時のフィルタリング
device.i2cWrite(MPR121ADDR, 0x2B, 0x01); // MHD_R
device.i2cWrite(MPR121ADDR, 0x2C, 0x01); // NHD_R
device.i2cWrite(MPR121ADDR, 0x2D, 0x00); // NCL_R
device.i2cWrite(MPR121ADDR, 0x2E, 0x00); // FDL_R
// Section B: dataがベースラインより小さい時のフィルタリング
device.i2cWrite(MPR121ADDR, 0x2F, 0x01); // MHD_F
device.i2cWrite(MPR121ADDR, 0x30, 0x01); // NHD_F
device.i2cWrite(MPR121ADDR, 0x31, 0xFF); // NCL_L
device.i2cWrite(MPR121ADDR, 0x32, 0x02); // FDL_L
// Section C: 各電極(ELE0-11)の閾値(T:Touch,R:Release)の設定
device.i2cWrite(MPR121ADDR, 0x41, 0x0F); // ELE0_T
device.i2cWrite(MPR121ADDR, 0x42, 0x0A); // ELE0_R
device.i2cWrite(MPR121ADDR, 0x43, 0x0F); // ELE1_T
device.i2cWrite(MPR121ADDR, 0x44, 0x0A); // ELE1_R
device.i2cWrite(MPR121ADDR, 0x45, 0x0F); // ELE2_T
device.i2cWrite(MPR121ADDR, 0x46, 0x0A); // ELE2_R
device.i2cWrite(MPR121ADDR, 0x47, 0x0F); // ELE3_T
device.i2cWrite(MPR121ADDR, 0x48, 0x0A); // ELE3_R
// Section D: フィルタの設定
device.i2cWrite(MPR121ADDR, 0x5D, 0x04);
// Section E: 電極の設定
device.i2cWrite(MPR121ADDR, 0x5E, 0x0C);
}

// 06音を再生する処理
rapiro.playSound = function (fname) {
exec('aplay -q ' + fname); // quietモード、callbackによるエラー処理はしていない
}

// 06音声合成の処理
rapiro.talk = function (txt) {
voice
.speaker('haruka') // haruka, hikari, takeru, santa, bear (, show)
.emotion('happiness') // happiness, anger, sadness
.emotion_level(2) // 1-4 (default:2)
.pitch(100) // 50-200% (default:100)
.speed(100) // 50-400% (default:100)
.volume(100) // 50-200% (default:100)
.speak(txt, function(err, buf) { // 音声合成開始
if (!err) {
fs.writeFile('voice.wav', buf, 'binary', function(err) { // wavファイル書き出し
if (!err) {
rapiro.playSound('voice.wav'); // wavファイル再生
}
});
}
});
}

// 06時刻を話す処理
rapiro.talkTime = function () {
const date = new Date(); // 現在時刻を取得
const txt = date.getHours() + '' + date.getMinutes() + '分です'; // 文字列に加工
rapiro.talk(txt); // 話させる
}

// 06天気を話す処理(都市ID, 天気情報の開始日[0-4], 終了日[0-4])
rapiro.talkWeather = function (city, dayFrom, dayTo) {
weather.setUnits('metric'); // 単位
weather.setLang('en'); // 言語
weather.setCityId(city); // 都市ID
weather.getWeatherForecastForDays(dayTo + 1, function(err, obj){ // dayTo日後までの天気情報を得る
let txt = obj.city.name + 'の天気を調べます。'; // 話す文字列
rapiro.talk(txt); // いったん話す(音声合成を待つ)
txt = '';
for (let d = dayFrom; d <= dayTo; d++) { // 指定日数分の文字列を作る
txt += dateString(obj.list[d].dt); // 日付
txt += '';
txt += weatherIdToDescription(obj.list[d].weather[0].id); // 天気の呼び
txt += '、最高気温は';
txt += Math.round(obj.list[d].temp.max); // 最高気温(四捨五入)
txt += '度。';
}
rapiro.talk(txt); // 天気情報を話す
});
}

// 06 UNIX時間を日付文字列に変換する関数(dtを入れると「○月○日 ○曜日」になる)
function dateString(dt) {
const date = new Date(dt * 1000);
const mon = date.getMonth() + 1;
const day = date.getDate();
const dno = date.getDay();
const dname = ['', '', '', '', '', '', ''];
const str = mon + '' + day + '' + dname[dno] + '曜日';
return str;
}

// 06 OpenWeatherMapの天気IDを日本語の呼びにする関数
// http://openweathermap.org/weather-conditions
function weatherIdToDescription(weatherId) {
const cond = [];
cond[200] = '弱い雨を伴った雷';
cond[201] = '雷雨';
cond[202] = '強い雨を伴った雷';
cond[210] = '弱い雷';
cond[211] = '';
cond[212] = '強い雷';
cond[221] = 'ときどき雷';
cond[230] = '弱い霧雨を伴った雷';
cond[231] = '霧雨を伴った雷';
cond[232] = '強い霧雨を伴った雷';
cond[300] = '薄い霧雨';
cond[301] = '霧雨';
cond[302] = '濃い霧雨';
cond[310] = '弱めにしとしと降る霧雨';
cond[311] = 'しとしと降る霧雨';
cond[312] = '強めにしとしと降る霧雨';
cond[313] = 'にわか雨と霧雨';
cond[314] = '強いにわか雨と霧雨';
cond[321] = 'にわか雨と霧雨';
cond[500] = '小雨';
cond[501] = '';
cond[502] = '強い雨';
cond[503] = '非常に強い雨';
cond[504] = '猛烈な雨';
cond[511] = '冷たい雨';
cond[520] = '弱いにわか雨';
cond[521] = 'にわか雨';
cond[522] = '強いにわか雨';
cond[531] = 'ときどき雨';
cond[600] = '弱い雪';
cond[601] = '';
cond[602] = '強い雪';
cond[611] = 'みぞれ';
cond[612] = 'にわかみぞれ';
cond[615] = '弱い雨または雪';
cond[616] = '雨または雪';
cond[620] = '弱いにわか雪';
cond[621] = 'にわか雪';
cond[622] = '強いにわか雪';
cond[701] = '';
cond[711] = 'かすみ';
cond[721] = 'もや';
cond[731] = 'つむじ風';
cond[741] = '濃霧';
cond[751] = '';
cond[761] = 'ほこり';
cond[762] = '火山灰';
cond[771] = 'スコール';
cond[781] = '竜巻';
cond[800] = '晴れ';
cond[801] = '少し曇り';
cond[802] = 'ときどき曇り';
cond[803] = '曇り';
cond[804] = '曇り';
cond[900] = '竜巻';
cond[901] = '熱帯の嵐';
cond[902] = 'ハリケーン';
cond[903] = '寒い';
cond[904] = '暑い';
cond[905] = '風が強い';
cond[906] = 'あられ';
cond[951] = '静か';
cond[952] = '弱い風';
cond[953] = '穏やかな風';
cond[954] = '';
cond[955] = 'さわやかな風';
cond[956] = '強めの風';
cond[957] = '強風';
cond[958] = '暴風';
cond[959] = '深刻な暴風';
cond[960] = '';
cond[961] = '猛烈な嵐';
cond[962] = 'ハリケーン';
return cond[weatherId];
}



操作インタフェース(index.html)

前回からの主な改編部分は以下


  • 64~75行目:時刻と天気を話させるボタン

  • 96, 97行目:そのボタンの取得

  • 160~165行目:ボタンを押したらsocketを送る処理


index.html

<!DOCTYPE html>

<!--
// Rapiro制御インタフェース
// Rapiro標準の10モーション実装
// socket.ioによりブラウザから制御
// 距離センサ(GP2Y0A21YK)による動作制御
// タッチセンサ(Grove I2C MPR121)による動作制御
// 時刻と天気を話させる
// 2017.01.29 by Mitsuteru Kokubun
-->

<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>rapiro.js 05: タッチセンサ</title>
<style>
td.center {text-align: center}
</style>
</head>

<body>
<table>
<!-- Rapiroからのレスポンスを表示する場所 -->
<tr>
<td class='center' colspan='3' id='msg'>motion / face</td>
</tr>
<!-- Rapiroの距離センサの値を表示する場所 -->
<tr>
<td class='center' colspan='3' id='prox'>proximity [cm]</td>
</tr>
<!-- Rapiroのタッチセンサの値を表示する場所 -->
<tr>
<td class='center' colspan='3' id='touch'>touch [0, 1, 2, 3]</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>
<tr><td>&nbsp;</td></tr>
<!-- 06時刻を話させるボタン -->
<tr>
<td class='center' colspan='3' id='touch'>
<input type='button' value='いま何時?' id='btnTime'>
</td>
</tr>
<!-- 06天気情報を話させるボタン -->
<tr>
<td class='center' colspan='3' id='touch'>
<input type='button' value='お天気は?' id='btnWeather'>
</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ボタン
var btnTime = document.getElementById('btnTime'); // 06時刻ボタン
var btnWeather = document.getElementById('btnWeather'); // 06天気ボタン

// 各ボタンをクリックした時の処理
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'
});
});
btnTime.addEventListener('click', function() { // 06時刻
socket.emit('time');
});
btnWeather.addEventListener('click', function() { // 06天気
socket.emit('weather');
});

// Rapiroからのレスポンスの表示
socket.on('response', function(data) { // socketでresponseイベントが届いたら
var msg = document.getElementById('msg'); // div要素を取得
msg.innerHTML = data.motion + ' / ' + data.face; // 動作と表情を表示
});

// Rapiroの距離センサの値の表示
socket.on('proximity', function(data) {
var prox = document.getElementById('prox');
prox.innerHTML = data.distance + ' cm'
});

// Rapiroのタッチセンサの値の表示
socket.on('touch', function(data) {
var touch = document.getElementById('touch');
touch.innerHTML = data.touch
});

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



動作確認


  • 上記3つのコードと効果音ファイル3個を Rapiro 内部の Raspberry Pi に FTP


    • e.g. /home/pi/rapiro/ 内に

    • 効果音は「効果音ラボ」から以下の3個をダウンロード


      • decision22.wav

      • decision24.wav

      • warning1.wav





  • SSH で Rapiro 内部の Raspberry Pi にアクセスし、node で app.js を実行


    • 準備ができると Rapiro のサーボに通電され直立状態となり、同時に起動音が鳴る



pi@raspberrypi:~/rapiro $ node app.js


  • 各 Feeler に触れるとタッチ音が鳴る


    • Feeler #0(右の角)に触れると天気情報を話す


      • この例では名古屋市の翌日・翌々日の天気と気温



    • Feeler #1(右の耳)に触れると現在時刻を話す



  • 以下、動作の様子(動画)

Rapiro を node.js (johnny-five + socket.io) で制御 - 音声合成と天気予報


  • PC や スマホのブラウザで、Raspberry Pi のIPアドレスにアクセス

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


  • [いま何時?] を押すと現在時刻を話す

  • [お天気は?] を押すと天気情報を話す


    • この例では名古屋市の翌日の天気と気温




まとめ

天気予報のように長い音声の場合、VoiceText Web API で音声を生成(ダウンロード?)するのに時間がかかり、かなり間延びした感じにはなってしまいます。これは、私がスマホのテザリング(運悪く通信速度制限中(汗))で Rapiro をつないでいることも関係しそうです。そのあたりの実用性については、これはあくまで「遊び」ということで気にしていません。

なお、音声合成には Open JTalk も試しましたが、個人的には、合成された音声は HOYA の VoiceText のほうが好みでした。

今回 Rapiro にしゃべらせていること(時刻と天気予報)は、実際は、ロボットに話させる必要性は低いでしょう。そのような情報は、スマホで確認したほうが早くて正確でしょう。

また、Rapiro がしゃべっているというより、正確には、Raspberry Pi で音声合成しているだけの話です。Rapiro が賢くなったわけではありません。

にもかかわらず、Rapiro がとても愛おしく感じられるのは、不思議です。単に画面(GUI)でインタラクションするより、ロボットを通じてインタラクションする Robotic User Interface (RUI) のほうが、不思議と楽しいですね。

次回以降は、この RUI をさらに推し進めて、例えば以下のようなことに挑戦していきたい考えです(あくまで考え)。


  • 赤外線による家電の操作

  • カメラの搭載、ストリーミング

  • 音声認識

  • 人工知能?による簡単な会話

  • etc.

最新コードは以下にあります。

https://github.com/mkokubun/rapiro.js