0
1

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 で制御 [6] 音声合成と天気予報

Last updated at Posted at 2017-01-29

やりたいこと

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

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

今回 [6] の目標

  • 多くの Rapiro ファンが実施していらっしゃる、音声合成と天気予報に挑戦
  • そのために Rapiro にスピーカーを搭載

方法

機材

  • Rapiro
  • 電源を入れる前に、こちらの手順で Arduino IDE を用いて、制御ボードに StandardFirmataPlus を書き込んでおく
  • Raspberry Pi 3 Model B
  • Rapiro 内部に搭載済(こちらの手順で)
  • Node.js インストール済(こちらの手順で)
  • npm で johnny-five, socket.io, express, voicetext, openweather-apis をインストール済
  • エネループ(単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

0
1
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?