LoginSignup
3
2

More than 5 years have passed since last update.

二足歩行ロボット Rapiro を Node.js で制御 [7] 家電の操作

Last updated at Posted at 2017-01-30

やりたいこと

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

  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 をベースに、さらに賢そうな遊び方を模索する
    • [6]で完了)スピーカーの搭載、音声合成
    • [6]で完了)天気予報
    • 赤外線による家電の操作
    • カメラの搭載、ストリーミング
    • 音声認識
    • 人工知能?による簡単な会話
    • etc.

今回 [7] の目標

方法

機材

  • Rapiro
    • 電源を入れる前に、こちらの手順で Arduino IDE を用いて、制御ボードに StandardFirmataPlus を書き込んでおく
  • Raspberry Pi 3 Model B
  • エネループ(単3)5本 または Rapiro 用 ACアダプタ
  • PC(Windows10、Raspberry Pi に SSH や FTP できればなんでも良い)
  • スマホまたはタブレット
  • 無線LAN環境
  • [4]から距離センサ搭載済
  • [5]からタッチセンサ搭載済
  • [6]からスピーカー搭載済

irMagician の設置

  • irMagician を Rapiro に USB で接続
  • 下の写真のように、右腕に養生テープで貼り付け IMG_0147.JPG

コーディング

概要

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

    • rapiro-cfg.js : Rapiro の各種設定を格納するオブジェクト(前回と同一)
    • app.js : 制御プログラム本体
    • index.html : 操作インタフェース
      • 今回は app.js と index.html のみ前回から後述のように改編
  • npm でインストールされる irmagician モジュールは温度センサに対応していないが、GitHub には対応途上?のコードがあるため、そちらのコードを参考に、irMagician.js を少し修正

  • 動作の概要

    • htmlのボタンを押すと、Rapiro が右腕を上げて、テレビやエアコンに赤外線を送信
    • html上に温度が表示される

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

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

  • 42行目:温度を格納する変数
  • 59行目:irmagician モジュールの設定
  • 266~273行目:htmlからの赤外線送信のsocketの処理
  • 452~458行目:一定間隔で温度データを取得してsocketで送る処理
  • 460~466行目:irMagician から赤外線を送信する処理
app.js
// rapiro.js 07
// 家電の操作
// irMagician-T の利用
//   ・テレビとエアコンを操作(この例では東芝のテレビと富士通のエアコン)
//   ・[04]距離センサと[05]静電容量タッチセンサ搭載前提
//   ・モジュール irmagician をインストールしておく
//      npm install irmagician
//      https://github.com/tamaki-shingo/irmagician
//      ※ 温度を取得するには irMagician.js を少し改編が必要
// 2017.01.30 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],
    temp:     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

// irMagician の設定
const irMagician = require('irmagician');       // irmagician モジュールを使う


// 制御ボードの準備ができたら
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('./snd/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('./snd/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('./snd/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('./snd/decision22.wav');   // タッチ音の再生
    });
    socket.on('weather', function(data) {           // 06天気を話す
        rapiro.talkWeather(city, 1, 1);             // 翌日の天気のみ
        rapiro.playSound('./snd/decision22.wav');   // タッチ音の再生
    });
    socket.on('ir', function(data) {                // 07赤外線の送信
        rapiro.sendIR('./ir/' + data.name + '.json');
        rapiro.playSound('./snd/decision22.wav');   // タッチ音の再生
        rapiro.execMotion('yellow');                // 右手を上げる
        setTimeout(function() {
            rapiro.execMotion('stop');              // しばらくしたら停止状態にする
        }, 4000);
    });
});

// ブラウザに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 + 'の天気。';                           // 話す文字列
        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];
}

// 07 一定間隔で irMagician の温度を取得する
setInterval(function() {
    rapiro.temp = irMagician.temp() - 8.4;  // 温度の取得(適当に補正)
    emitSocket('temp', {
        temp: rapiro.temp
    });
}, 5000);                                   // 5秒間隔

// 07 irMagician で赤外線を送信する関数
rapiro.sendIR = function (fileName) {       // ファイル名(*.json)を指定
    irMagician.write(fileName);             // irMagician のメモリに書き込む
    setTimeout(function() {                 // 少し待ってから
        irMagician.play();                  // irMagician で赤外線を送信
    }, 2000);                               // 例えばメモリ書き込みから2秒後
};

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

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

  • 35~37行目:温度を表示する場所
  • 78~83行目:テレビやエアコンを操作するボタン
  • 105~108行目:それらのボタンの取得
  • 178~186行目:ボタンを押したらsocketを送る処理
  • 201~204行目:socketで送られてきた温度を表示する処理
index.html
<!DOCTYPE html>
<!--
// Rapiro制御インタフェース
// Rapiro標準の10モーション実装
// socket.ioによりブラウザから制御
// 距離センサ(GP2Y0A21YK)による動作制御
// タッチセンサ(Grove I2C MPR121)による動作制御
// 時刻と天気を話させる
// irMagician-T による家電制御と温度表示
// 2017.01.30 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}
        td.right  {text-align: right}
        td.left   {text-align: left}
    </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>
        <!-- 07 Rapiroの温度センサの値を表示する場所 -->
        <tr>
            <td class='center' colspan='3' id='temp'>temperature [℃]</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='right'> <input type='button' value='←' id='btnLeft'></td>
            <td class='center'><input type='button' value='■' id='btnStop'></td>
            <td class='left'>  <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='time' id='btnTime'>
                <input type='button' value='weather' id='btnWeather'>
            </td>
        </tr>
        <!-- 07赤外線を送信させるボタン -->
        <tr>
            <td class='center'><input type='button' value='tvPow' id='btnTvPow'></td>
            <td class='center'><input type='button' value='acOn' id='btnAcOn'></td>
            <td class='center'><input type='button' value='acOff' id='btnAcOff'></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天気ボタン
        var btnTvPow   = document.getElementById('btnTvPow');   // 07テレビ電源ボタン
        var btnAcOn    = document.getElementById('btnAcOn');    // 07エアコンONボタン
        var btnAcOff   = document.getElementById('btnAcOff');   // 07エアコンOFFボタン


        // 各ボタンをクリックした時の処理
        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');
        });
        btnTvPow.addEventListener('click', function() {     // 07テレビの電源
            socket.emit('ir', {name: 'tvPow'});
        });
        btnAcOn.addEventListener('click', function() {      // 07エアコンON
            socket.emit('ir', {name: 'acOn'});
        });
        btnAcOff.addEventListener('click', function() {     // 07エアコンOFF
            socket.emit('ir', {name: 'acOff'});
        });

        // 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 = Math.round(data.distance) + ' cm'
        });

        // 07 Rapiroの温度センサの値の表示
        socket.on('temp', function(data) {
            var temp = document.getElementById('temp');
            temp.innerHTML = Math.round(data.temp) + '';
        });

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

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

irMagician.js の修正

GitHub のコードから以下の部分をコピーして、/node_modules/irmagician/lib/ 内の irMagician.jsを修正

  • 温度を格納する変数 celsiusTemp をグローバルで宣言
  • temp 関数内で、celsiusTemp に値を格納(logはコメントアウト)
  • exports.temp 関数内で、celsiusTemp を return
irMagician.js
var celsiusTemp    // 新たに宣言

function temp() {
    return new Promise((resolve, reject) => {
        this.port.write("T\r\n", (error, bytesWritten) => {
            var logMessage = ""
            this.port.on("data", data => {
                var msg = data.toString().split(/\r\n/)
                if (msg[0]) {
                    var temp = msg[0]
                    //var celsiusTemp = ((5.0 / 1024.0 * temp) - 0.4) / 0.01953 // コメントアウト
                    celsiusTemp = ((5.0 / 1024.0 * temp) - 0.4) / 0.01953       // 追加
                    //console.log(celsiusTemp)                                  // コメントアウト
                    resolve()
                } else {
                    resolve()
                }
            })
            if (error) {
                console.log("Error: ", error.message)
                reject(error)
            }
        })
    })
}

exports.temp = (path, port) => {
    co(function* () {
        var validPort = yield autoSelectDevicePort()
        port = port || validPort
        yield open(port)
        yield temp(path)
        yield close()
    }).catch(error => {
        console.log('💀  Error: ${error}')
        close()
    })
    return celsiusTemp;    // 新たに追加
}

動作確認

  • 上記3つのコードを Rapiro 内部の Raspberry Pi に FTP
    • e.g. /home/pi/rapiro/ 内に
  • irMagician の赤外線のデータは事前に読み取り・ファイルを作成しておき、/home/pi/rapiro/ir/ 内に入れる
    • ファイルの作成方法は irmagician モジュール開発者のページをご参考
    • 今回は東芝のテレビと富士通のエアコンのリモコンについて、いくつかの赤外線を読み取っておきました
    • 例えば tvPow.json, acOn.json, acOff.json などのファイル名で
  • 効果音は「効果音ラボ」から以下の3個をダウンロードし、/home/pi/rapiro/snd/ 内に入れる
    • decision22.wav
    • decision24.wav
    • warning1.wav
  • SSH で Rapiro 内部の Raspberry Pi にアクセスし、node で app.js を実行
    • 準備ができると Rapiro のサーボに通電され直立状態となり、同時に起動音が鳴る
pi@raspberrypi:~/rapiro $ node app.js
  • PC や スマホのブラウザで、Raspberry Pi のIPアドレスにアクセス
http://192.168.**.***:3000
  • [tvPow] を押すと Rapiro が右腕を上げ、テレビの電源を ON/OFF させる赤外線を送信
  • [acOn] を押すと同様にエアコンの電源をON、[acOff]押すとエアコンの電源をOFFにする

Rapiro を node.js (johnny-five + socket.io) で制御 - 家電の操作

まとめ

Rapiro にやらせるよりも、自分でリモコンを操作したほうが早いです(笑)
あくまで「遊び」ということで。

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

  • カメラの搭載、ストリーミング
  • 音声認識
  • 人工知能?による簡単な会話
  • etc.

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

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