スマートフォンで学習リモコンを操作して、エアコンやスピーカー等の機器をコントロールしたいと思います。
学習リモコン本体は、Raspberry Piに学習リモコン基板を取り付け、部屋の中央付近(機器に赤外線が届く位置)に設置します。
スマートフォンでリモコンのボタンを表示しますが、ボタンの登録もスマートフォンで行えるように設定画面も作成しました。
シンプルにRaspberry PiがWebサーバとして動作しますので、外部のサーバ契約やアカウント登録などは一切不要です。このような学習リモコンシステムの構築手順を記録しておこうと思います。
目次
- 1 Raspberry Pi学習リモコンWebインタフェース構築手順
Raspberry Pi学習リモコンWebインタフェース構築手順
使用する機材
Raspberry Pi本体
Node.jsを動かす都合上、Raspberry Pi 2以降が良さそうです(無印は動作速度的に非推奨のようです)。スマートフォンの接続は、Raspberry Piに有線LANケーブルを接続し、Wi-Fiルータを経由して行いました。学習リモコン基板
ビット・トレード・ワンさんのこちらの基板を使用させて頂きました。IR(赤外線)の学習と送信を行います。microSDカード
余ったclass 4のmicroSDカードを利用しましたが、動作が遅い感じでした。UHS-I対応の高速なmicroSDカードをお勧め致します。学習リモコンGUI構築手順
全体的な流れは、OSインストール→学習リモコン用コマンドインストール→Node.js環境構築→JavaScript、HTML作成→実行・動作確認になります。
1. Raspbianインストール
こちらの記事を参考に、Raspbianのインストールと初期設定を行います。
構築・動作確認は「2018-06-27-raspbian-stretch.zip」で行いました。
2. I2C有効化
raspi-configでI2Cを有効化、再起動しておきます。
sudo raspi-config
3. 学習リモコン用ソフトウェアのインストール
piユーザでログイン、設定作業を行います。
ビット・トレード・ワンさんのホームページで配布されています「ラズパイ専用学習リモコン対応ソフトウェア」を使用させて頂きました。zipファイルを取得し展開しました。
wget http://bit-trade-one.co.jp/wp/wp-content/uploads/2018/05/I2C0x52-IR-IR20180413-4.zip
unzip I2C0x52-IR-IR20180413-4.zip
4. node.jsインストール
画面はnode.jsで作成します。node.jsとnpmをインストールします。
sudo apt install nodejs npm
node.jsからpython3を起動し、先程展開したIR-remocon02-commandline.pyを実行するかたちです。
python3とpython3-smbusは既定でインストール済みでした。
5. プロジェクト作成
npmでexpress-generatorをインストール後、expressコマンドでexpressプロジェクトを作成しました。
mkdir piRcSv
cd piRcSv
sudo npm install -g express-generator
express --view=ejs
npm install
念の為、npm startでWebサーバを実行、
DEBUG=pircsv:* npm start
他のPCからWebブラウザでアクセスできるか確認しました。
http://<Raspberry PiのIPアドレスまたはホスト名.local>:3000/
6. サーバ側JavaScript作成
app.js
既定のapp.jsを変更して、var api行とapp.use(‘/api’)行を追加しました。
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var logger = require('morgan');
var indexRouter = require('./routes/index');
// 追加1
var api = require('./routes/api');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
// 追加2
app.use('/api', api);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
routes/index.js
トップページを/IrSend.htmlに変更します。
var express = require('express');
var router = express.Router();
var path = require('path');
/* GET home page. */
router.get('/', function(req, res, next) {
res.sendFile(path.join(__dirname + "/../public/IrSend.html"));
});
module.exports = router;
routes/api.js
新たにapi.jsファイルを作成します。WebブラウザとRaspberry Piの間で、リモコンデータ(ボタンのリスト)の送受信と、赤外線データ(ボタンを押したときの信号)の送受信を行う内容です。
IR-remocon02-commandline.pyを別の位置に展開した場合、7行目のvar irCmdを変更して下さい。
var express = require('express');
var router = express.Router();
var jsonfile = require('jsonfile');
var exec = require('child_process').exec;
var irCmd = "/usr/bin/python3 /home/pi/I2C0x52-IR/IR-remocon02-commandline.py";
// リモコンデータを返します
router.get('/rclist', function(req, res) {
// 同期読込
var rcdata = jsonfile.readFileSync('rclist.json', {
encoding: 'utf-8'
});
console.log(rcdata);
//送信
res.json(rcdata);
});
// リモコンデータを保存します
router.post('/rclist', function(req, res) {
console.log(req.body);
//保存
try {
jsonfile.writeFileSync('rclist.json', req.body, {
encoding: 'utf-8'
});
}
catch(e) {
res.json({ 'result': 'error' });
return;
}
//返信
res.json({ 'result': 'done' });
});
// IR(赤外線)を送信します
router.get('/sendir/:ir', function(req, res) {
console.log(req.params);
//IR送信
var cmd = irCmd + ` t "${req.params.ir}"`;
exec(cmd, function(err, stdout, stderr) {
if (err !== null) {
res.json({ 'result':'error', 'stderr':stderr });
return;
}
res.json({ 'result': 'done' });
});
});
// 学習済のIRを返します
router.get('/ir/:id', function(req, res) {
console.log(req.params);
//IR読込
var cmd = irCmd + ` r ${req.params.id}`;
exec(cmd, function(err, stdout, stderr) {
if (err !== null) {
res.json({ 'result':'error', 'stderr':stderr });
return;
}
res.json({ 'result':'done', 'ir' : stdout });
});
});
module.exports = router;
users.jsとindex.ejsは使用しないので削除しました。
rm routes/users.js views/index.ejs
7.HTML,CSS作成
public/IrSend.html
リモコンのメイン画面を作成します。vue.jsを使用させて頂きました。
<!DOCTYPE HTML>
<html>
<head>
<title>Rasberry Pi 学習リモコン</title>
<link rel="stylesheet" href="/stylesheets/style.css">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
</head>
<body>
<div id="app">
<form>
<div class="rcbtn_wrap">
<button type="button" class="rcbtn"
v-for="item in rcdata.rclist"
v-on:click="onClickRcBtn(item)">
{{ item.name }}</button><br>
</div>
</form>
<!-- p>data : {{ $data }}</p -->
</div>
<p style="text-align: center;"><a href="/IrEdit.html">リモコン設定へ</a></p>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
<script src="javascripts/piir.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
'rcdata': {}
},
methods: {
onClickRcBtn: function(item) {
sendIrData(item.ir);
}
}
});
document.addEventListener('DOMContentLoaded', function() {
var fnRecv = function(data) {
app.rcdata = data;
};
receiveRcData(fnRecv);
}, false);
</script>
</body>
</html>
public/IrEdit.html
ボタンの編集画面を作成します。
<!DOCTYPE HTML>
<html>
<head>
<title>リモコン設定</title>
<link rel="stylesheet" href="/stylesheets/style.css">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
</head>
<body>
<div id="app">
<form style="margin: 5%;">
<ol>
<li v-for="(item, index) in rcdata.rclist">
<button type="button" v-on:click="onClickUpBtn(item, index)">↑</button>
<button type="button" v-on:click="onClickDownBtn(item, index)">↓</button>
<button type="button" v-on:click="onClickDeleteBtn(item, index)">削除</button>
{{ item.name }}
<button type="button" v-on:click="onClickIrSendBtn(item, index)">IR(赤外線)送信</button>
</li>
</ol>
<p>
①学習リモコン基板(ADRSIR)に赤外線信号を登録して下さい。<br>
<label>登録先のボタン<input ref="btnNo" v-model="btnNo" type="number" min="1" max="10" size="2">番</label>(1~10番)<br>
<label>②ボタン名を入力して下さい。<input ref="btnName" v-model="btnName" type="text"></label><br>
<button type="button" v-on:click="onClickAddBtn">ボタン追加</button>
<p>ボタン追加時、赤外線信号はJSONファイルに保存されます。<br>
ADRSIRの同じボタンで繰り返し登録・追加しても問題ありません。</p>
</p>
</form>
<!-- p>data : {{ $data }}</p -->
</div>
<p style="text-align: center;"><a href="/IrSend.html">リモコン画面へ</a></p>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>
<script src="javascripts/piir.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
'rcdata': {},
'btnNo': '1',
'btnName': ''
},
methods: {
onClickUpBtn: function(item, index) {
var arr = this.rcdata.rclist;
if (index <= 0) {
return;
}
arrswap(arr, index);
//リモコンリストをサーバにPOSTしてファイル保存
sendRcData(this.rcdata);
},
onClickDownBtn: function(item, index) {
var arr = this.rcdata.rclist;
if (index >= (arr.length-1)) {
return;
}
arrswap(arr, index+1);
sendRcData(this.rcdata);
} ,
onClickDeleteBtn: function(item, index) {
if(! confirm('「'+item.name+'」ボタンを本当に削除しますか?')) {
return;
}
var arr = this.rcdata.rclist;
arr.splice(index, 1);
sendRcData(this.rcdata);
},
onClickIrSendBtn: function(item, index) {
sendIrData(item.ir);
},
onClickAddBtn: function() {
if(! app.btnNo || app.btnNo < 1 || app.btnNo > 10 ) {
alert("正しいボタンの番号を入力してくださいな。");
this.$refs.btnNo.focus();
return;
}
if(! app.btnName) {
alert("ボタンに名前をつけてあげましょう。");
this.$refs.btnName.focus();
return;
}
var fnRecv = function(data) {
console.log(data);
if(data.result !== "done") {
alert("赤外線データを読み込めませんでした。");
return;
}
var newRc = {
'name': app.btnName,
'ir': data.ir
};
console.log(newRc);
var arr = app.rcdata.rclist;
if(! arr) {
console.log("create new rclist");
app.rcdata = {
'rclist': [newRc]
};
} else {
arr.push(newRc);
}
console.log(app.rcdata);
sendRcData(app.rcdata);
};
var btnId = parseInt(app.btnNo) -1;
receiveIrData(btnId, fnRecv);
}
}
});
var arrswap = function(arr, index) {
arr.splice(index-1, 2, arr[index], arr[index-1]);
};
// ページ読み込み時、リモコンリストを読み込みます
document.addEventListener('DOMContentLoaded', function() {
var fnRecv = function(data) {
app.rcdata = data;
};
receiveRcData(fnRecv);
}, false);
</script>
</body>
</html>
public/stylesheets/style.css
スタイルシートでボタンを大きめに表示するようにしました。
body {
width: auto;
-webkit-text-size-adjust: 100%;
}
a {
color: #00B7FF;
}
.rcbtn {
font-size: x-large;
width: 80%;
padding-top: 10px;
padding-bottom: 10px;
margin-top: 10px;
margin-bottom: 10px;
}
.rcbtn_wrap {
text-align:center;
}
8. クライアント側JavaScript作成
public/javascripts/piir.js
リモコンデータ(ボタンのリスト)の送受信と、赤外線データ(ボタンを押したときの信号)の送受信のWebブラウザ側のコードになります。
var xhrCheckState = function(tgt, xhr) {
if(xhr.readyState !== 4) {
console.log(tgt + '中:' + xhr.readyState);
return false;
}
if(xhr.status !== 200) {
console.error(tgt + 'エラー:' + xhr.status);
return false;
}
return true
};
var receiveRcData = function(fnRecv)
{
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(! xhrCheckState('リモコンリスト受信', xhr)) {
return;
}
console.log(xhr.responseText);
fnRecv(JSON.parse(xhr.responseText));
};
xhr.open('GET','api/rclist', true);
xhr.send(null);
};
var sendRcData = function(rcdata) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(! xhrCheckState('リモコンリスト送信', xhr)) {
return;
}
console.log(xhr.responseText);
};
var rcjdata = JSON.stringify(rcdata);
console.log(rcjdata);
xhr.open('POST','api/rclist/', true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(rcjdata);
};
var receiveIrData = function(btnId, fnRecv) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(! xhrCheckState('IR受信', xhr)) {
return;
}
console.log(xhr.responseText);
fnRecv(JSON.parse(xhr.responseText));
};
xhr.open('GET','api/ir/' + btnId, true);
xhr.send(null);
};
var sendIrData = function(irdata) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(! xhrCheckState('IR送信', xhr)) {
return;
}
console.log(xhr.responseText);
};
xhr.open('GET','api/sendir/' + irdata, true);
xhr.send(null);
};
9. モジュールの追加
jsonfileを使用しましたので、プロジェクトにモジュールを追加します。
npm install jsonfile --save
10. 空のリモコンデータの作成
リモコンのデータは、単純にファイルに保存しています。空のjsonファイルを作成しておきます。
echo {} > rclist.json
リモコンの編集画面でボタンを登録すると、ファイルにボタンの名前と赤外線のデータが追加されます。
学習リモコンサーバ起動
以上で準備が完了しました。node.jsでWebサーバを起動します。(デバッグ実行ですが)
cd piRcSv/
DEBUG=pircsv:* npm start
しばらく待つと、TCPポート3000番でWebサーバが起動します。ポート番号はbin/wwwファイルで設定されています。Webブラウザでアクセスすると、リモコンの画面が表示されますが・・・最初は空です。
http://<Raspberry PiのIPアドレスまたはホスト名.local>:3000/
「リモコン設定へ」をクリックします。ボタンの登録手順は以下になります。
- 学習リモコン基板のスライドスイッチを「LERN」にセットします。
- 任意のスイッチ(ボタン)を押します。
- リモコンを操作し、赤外線信号を記憶させます。
- スライドスイッチを戻します。
- リモコン設定画面の①に、2番で記憶させたボタンの番号を入力します。
- ②にボタンの名前を入力します。
- 「ボタン追加」をクリックします。
このような感じで、ボタンが追加されます。
リモコン画面に戻ると、このような感じになります。
以上で構築が完了しました。
まあ、ざっと作った作りたての感じで、ボタンのレイアウトがリモコンぽくありません。
ボタンの大きさや位置、色などは、スタイルシートを使って、個々のボタンのスタイルを設定することで工夫できそうです。(プログラムを変更して、スタイル名の入力欄を付ける必要がありそうです)
デバッグモードではなく、node.jsでRaspberry Pi用の本番環境を構築する場合、どうやるのでしょう?・・・追々調べてみようと思います。
ちなみにWebサーバとして実装しましたので、外出先から自宅へVPNで接続して、エアコンの電源を入れておくような使い方もできそうですが・・・火事などには十分お気をつけ下さい。
PM2の本番環境構築
下記の手順でRaspberry PiにPM2をインストールして、本番環境として動かすことができました。追記しておきます。
# 最新版のnpmをインストール
sudo npm install -g npm@latest
# PM2インストール
sudo npm install -g pm2
# PM2から学習リモコンサーバを起動
pm2 start npm -- start
# 情報表示
pm2 list
pm2 show 0