やりたいこと
最終目標(前回から引き継ぎ)
- ([3]で完了)
二足歩行ロボット Rapiro の制御を、標準の Arduino IDE(C/C++)ベースから、JavaScript(Node.js)ベースに移植し、Rapiro を IoT デバイスっぽくする
- ([1][2]で完了)
かつ、PCでの制御ではなく、Rapiro 内部に搭載した Raspberry Pi での制御とし、完全無線化を図る - ([3]で完了)
全機能の移植が難しくとも、最低限、10個の基本動作は移植・再現する
- ([5]で完了)
可能な限り、書籍「二足歩行ロボット 工作&プログラミング(リックテレコム)」の改造内容も移植する
- この Rapiro をベースに、さらに賢そうな遊び方を模索する
今回 [7] の目標
-
こちらで紹介されている、赤外線リモコンデバイス irMagician による家電の操作に挑戦
- irMagician の制御には irMagician モジュールを利用
- 温度センサ(Microchip MCP-97001)も付いた irMagician-T を使って温度も取得
- irMagician-T にも対応途中?の GutHub のコードも活用
方法
機材
- Rapiro
- 電源を入れる前に、こちらの手順で Arduino IDE を用いて、制御ボードに StandardFirmataPlus を書き込んでおく
- Raspberry Pi 3 Model B
- Rapiro 内部に搭載済(こちらの手順で)
- Node.js インストール済(こちらの手順で)
- npm で johnny-five, socket.io, express, voicetext, openweather-apis, irmagician をインストール済
- エネループ(単3)5本 または Rapiro 用 ACアダプタ
- PC(Windows10、Raspberry Pi に SSH や FTP できればなんでも良い)
- スマホまたはタブレット
- 無線LAN環境
- [4]から距離センサ搭載済
- [5]からタッチセンサ搭載済
- [6]からスピーカー搭載済
irMagician の設置
コーディング
概要
-
以下の三つのコードを入力し、同じ階層(e.g. /home/pi/rapiro/)に置く
-
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> </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> </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> </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 にやらせるよりも、自分でリモコンを操作したほうが早いです(笑)
あくまで「遊び」ということで。
次回以降はさらに遊びを推し進め、例えば以下のようなことに挑戦していきたい考えです(あくまで考え)。
- カメラの搭載、ストリーミング
- 音声認識
- 人工知能?による簡単な会話
- etc.
最新コードは以下にあります。
https://github.com/mkokubun/rapiro.js