エンジニアのはしがき

プログラミングの日々の知見を書き連ねているブログです

RaspberryPi+Node.js+Reactで赤外線リモコンをつくろう(1/2)

こんにちは!

仕事から帰ってくると部屋が寒い…エアコンが効くまでがツラい…そう思う時期になってきました。 せめて事前にエアコンを自宅外から操作できればいいのに…という思いから、今回は余っていたラズパイを引っ張り出して、赤外線リモコンアプリを作ってみました。

f:id:tansantktk:20201120001532p:plain

記事が長くなった為、2つに分割しています。

tm-progapp.hatenablog.com

参考

やりたいこと

  • スマホタブレットを家電リモコン化して、出先からでも家電を操作できるようにする。

完成物

Node.jsサーバ

github.com

Reactアプリ

github.com

↓実際のReactアプリ画面

f:id:tansantktk:20210408202746p:plain:w300

スマホやPCからアプリにアクセスし、エアコン等の家電の状態をタップして切り替えることができるようにしてます。

記事ではしっかりと紹介しきれなかったのですが、折角なので別ページにサインイン画面も用意し、Firebase Authenticationを利用してEmailとパスワードの組み合わせで認証を要求するように実装してみました。  

システム構成図

f:id:tansantktk:20201115112908p:plain

大きく分けて、Node.jsサーバとReactアプリの2つで構成しています。

↓のようなフローで家電を操作していきます。

  1. スマホタブレットからReactアプリでTVやエアコンなどのリモコンをON/OFF操作。 
  2. 操作した内容をRealtimeDatabaseに反映。
  3. ラズパイ側では常にNode.jsサーバを稼働させておき、RealtimeDatabaseの変更を検知して、Pythonコードを実行。
  4. PythonコードからGPIOを操作。
  5. GPIOから赤外線LEDに信号を送信。
  6. 送信した信号がTVやエアコンなどの家電に届き、ON/OFF操作が実行される。

GPIOは、ラズパイ側からデジタル信号を入出力するためのピンのことです。ラズパイの端っこに剣山のように生えているヤツのことですね。 今回は、赤外線を送る為にGPIOからデジタル信号を発信し、自作の電子回路から家電へ赤外線を飛ばします。

物理的に必要なもの

  • RasberryPi 3B+ ※ZeroWでもOKです
  • 赤外線リモコン受信モジュール PL-IRM0101(38kHz)シールド付
  • 5mm赤外線LED 940nm OSI5LA5113A グレー (10個入)
  • LED光拡散キャップ(5mm) 青 (50個入)※任意
  • ブレッドボード・ジャンパーワイヤ(オス-メス) 15cm(赤) (10本入)
  • ブレッドボード・ジャンパーワイヤ(オス-メス) 15cm(青) (10本入)
  • ブレッドボード・ジャンパーワイヤ(オス-メス) 15cm(黄) (10本入)
  • カーボン抵抗(炭素皮膜抵抗) 1/2W 4.7kΩ (100本入)
  • カーボン抵抗(炭素皮膜抵抗) 1W27Ω (100本入)
  • MOSFET 2N7000
  • PchパワーMOSFET(55V11A) IRFU9024NPBF
  • ブレッドボード(Amazonで適当なものをお好みで購入)
  • 操作したい家電製品のリモコン

↓回路用の部品はほぼ秋月電子通称で購入できます。

秋月電子通商 トップページ - 電子部品・半導体 【通販・販売】

Rasberry Pi - Node.jsサーバの実装

まずはラズパイにRaspbianをインストールし、インターネットに接続できる状態にしておきます。 (説明は省略)

ラズパイ開発環境

$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 9.9 (stretch)
Release:    9.9
Codename:   stretch

各種インストール

まずはいわゆるおまじないですね。各パッケージを最新にしておきます。

$ sudo apt update
$ sudo apt upgrade

次にGPIOを操作するライブラリ pigpio をインストールして、起動時に自動的に有効化されるように設定しておきます。

$ sudo apt install pigpio python3-pigpio
$ sudo systemctl enable pigpiod.service
$ sudo systemctl start pigpiod

nodejsもインストールします。 sudo apt install nodejsといきたいところですが、これでインストールしてしまうと古いバージョンのnode.jsが入ってしまう為、以降の処理で正しく動作しませんでした。(ここでドハマりし、数時間溶かした)

ということで、node.jsのバージョン管理ツールであるnvmをgit経由でインストールしましょう。

$ sudo mkdir ~/.nvm
$ sudo chmod 777 ~/.nvm
$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh # nvmをロード

上記を実行後に$ nvm --versionを実行して、バージョンが表示されればOKです。

このままだと、起動する毎にnvmを手動でロードする必要があるので、ブート時にnvmを自動的にロードするように設定しておきます。

$ sudo vim /etc/rc.local
# /etc/rc.local
export NVM_DIR="/home/pi/.nvm"  # .nvmディレクトリのパスを指定
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

nvm経由でnodeをインストールします。

$ nvm install v12.18.3

nodeと一緒にnpmもインストールされたことを確認しておきます。

$ node -v
v12.18.3
$ npm -v
6.14.6

GPIOの入出力設定を定義します。

$ crontab -e
# crontab
@reboot until echo 'm 17 w   w 17 0   m 18 r   pud 18 u' > /dev/pigpio; do sleep 1s; done

GPIOの各ピンにはそれぞれ決まった番号が振られており、↑のコードでは「17ピンをデジタル信号の出力用、18ピンをデジタル信号の入力用として使いますよ」と定義しています。 GPIOについては↓等が参考になるかと思います。

Raspberry Pi 2/3 B ピン配置 (40ピン) - Raspberry Pi - 基礎からの IoT 入門

家電リモコンの赤外線を取得する

家電リモコンの赤外線を操作する為にはまず、リモコンのボタンを押した際に発信される赤外線の値を事前に知っておく必要があります。 そこで、irrp.pyという便利なpythonライブラリを使い、家電リモコンの赤外線を取得してみます。

まずは赤外線を受信する為に以下の電子回路を作ります。 ラズパイが起動していると、GPIOには電気が流れますので、回路とラズパイをジャンパワイヤーで繋ぐ前にラズパイの電源は切っておきましょう。

f:id:tansantktk:20201114094639p:plain

次にirrp.pyを作業用の適当な場所にDLします。

$ curl http://abyz.me.uk/rpi/pigpio/code/irrp_py.zip | zcat > irrp.py

操作したい家電のリモコンを手元に用意した状態で下記を実行します。 roomLightOnの部分は赤外線の値に対するキーです。後でnode.jsサーバからの呼出時に使うので、リモコンのボタンに応じた分かりやすい名称を付けましょう。

$ python3 irrp.py -r -g18 -f codes roomLightOn --no-confirm --post 130

実行後は赤外線を受信するまで待機状態になるので、電子回路の赤外線リモコン受信モジュールに向けて、家電のリモコンの任意のボタンを押します。 受信が完了するとレスポンスが表示され、codesというファイルに受信した赤外線の値が出力されます。

// codes
{"roomLightOn": [1277, 428, 1277, ..., ] }

codesの中身を見るとirrp.pyを実行した際に指定したキーと読み込んだ赤外線の値の組み合わせが出力されているはずです。 数値の配列で記述されている部分が赤外線の値です。

もし、エアコンのON/OFFを操作したい場合は、エアコンの運転ボタンと停止ボタンの赤外線信号を別々に受信してcodesに追記していく必要があります。 受信作業が終わったら、今回組んだ電子回路は以降では不要です。

赤外線送信用の回路を組む

今度は赤外線送信をする為の電子回路を組みます。 こちらについては↓を参考に組ませてさせていただきました。

格安スマートリモコンの作り方

回路図

f:id:tansantktk:20210926175348p:plain

実際の回路

f:id:tansantktk:20210926175419j:plain

赤外線出力のテスト

この時点で正しく回路が動作するかターミナルからテストします。 以下はcodesに記録したroomLightOnというキーの赤外線を出力するコマンドです。

$ python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes roomLightOn

うまく動作すれば家電に赤外線が届き動作すると思います。 なお、操作対象の家電と回路の距離が2~3m以上離れると赤外線出力のパワー不足により届かないことがあるので、回路の置き場所を変えてみたりもしてみましょう。 赤外線LEDに「LED光拡散キャップ」を被せると赤外線の拡散方向が広がるらしいですが、私の環境ではあまり効果がありませんでした。

Node.jsサーバのコーディング

pythonでの赤外線での家電操作ができるようになりましたので、ここからは家電の状態をリアルタイムにデータベースで保持する為のプログラムを組んでいきます。

家電の状態を保持する為には、FirebaseのRealtimeDatabaseを使いますので、Firebaseのアカウント登録をし、今回のアプリ用のプロジェクトを新規作成しておきます。 (手順は公式を参照下さい)

JavaScript でのインストールと設定  |  Firebase Documentation

以降のNode.jsサーバの説明は、以下のGitHubのソースを前提に説明しますので、ローカルにDLしておきます。 github.com

前の手順で作成したcodesをmain.jsと同じ階層に配置します。GitHubソースコードのcodesは上書きします。

Firebaseの環境変数用ファイルをローカルに作成。

touch nodeEnv.js

firebaseのAPIを利用するための各種情報を記述。

// nodeEnv.js
module.exports = {
  apiKey: "************",
  authDomain: "************",
  databaseURL: "************",
  projectId: "************",
  storageBucket: "************",
  messagingSenderId: "************",
}

RealtimeDatabaseを監視するメインのプログラムは↓になります。

// main.js
const firebase = require("firebase");
const exec = require("child_process").exec;
const nodeEnv = require('./nodeEnv.js');

console.log(nodeEnv);

// firebase init
const firebaseConfig = {
  apiKey: nodeEnv.apiKey,
  authDomain: nodeEnv.authDomain,
  databaseURL: nodeEnv.databaseURL,
  projectId: nodeEnv.projectId,
  storageBucket: nodeEnv.storageBucket,
  messagingSenderId: nodeEnv.messagingSenderId,
};
firebase.initializeApp(firebaseConfig);

// DB参照を準備
const powerRef = firebase.database().ref().child('airconPower');
const bedroomLightRef = firebase.database().ref().child('bedroomLight');
const diningLightRef = firebase.database().ref().child('diningLight');
const tvRef = firebase.database().ref().child('tv');
let command;

// エアコンON/OFF
powerRef.on("value", function(snapshot){
  switch(snapshot.val()) {
    case null:
      console.log('error: snapshot.val() is null...<(+p+)>');
      break;
    case 'cool':
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes coolairOn";
      break;
    case 'hot':
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes hotairOn";      
      break;   
    default:
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes powerOff";
      break;
  }

  exec(command, function(err, stdout, stderr){
      console.log('execute => ' + command);
      if(!err) {  
          console.log("stdout: " + stdout);
          console.log("stderr: " + stderr);
      } else {
        console.log(err);
      }
  });
});

// 寝室ライト
bedroomLightRef.on("value", function(snapshot){
  switch(snapshot.val()) {
    case null:
      console.log('error: snapshot.val() is null...<(+p+)>');
      break;
    case 'on':
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes bedroomLightOn";
      break;
    case 'off':
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes bedroomLightOff";      
      break;   
    default:
      console.log('error: snapshot.val() is invalid value...<(+p+)>');
      break;
  }

  exec(command, function(err, stdout, stderr){
      console.log('execute => ' + command);
      if(!err) {  
          console.log("stdout: " + stdout);
          console.log("stderr: " + stderr);
      } else {
        console.log(err);
      }
  });
});

// ダイニングライト
diningLightRef.on("value", function(snapshot){
  switch(snapshot.val()) {
    case null:
      console.log('error: snapshot.val() is null...<(+p+)>');
      break;
    case 'on':
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes diningLightOn";
      break;
    case 'off':
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes diningLightOff";      
      break;   
    default:
      console.log('error: snapshot.val() is invalid value...<(+p+)>');
      break;
  }

  exec(command, function(err, stdout, stderr){
      console.log('execute => ' + command);
      if(!err) {  
          console.log("stdout: " + stdout);
          console.log("stderr: " + stderr);
      } else {
        console.log(err);
      }
  });
});

// TV
tvRef.on("value", function(snapshot){
  switch(snapshot.val()) {
    case null:
      console.log('error: snapshot.val() is null...<(+p+)>');
      break;
    case 'on':
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes tvOn";
      break;
    case 'off':
      command = "python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes tvOff";      
      break;   
    default:
      console.log('error: snapshot.val() is invalid value...<(+p+)>');
      break;
  }

  exec(command, function(err, stdout, stderr){
      console.log('execute => ' + command);
      if(!err) {  
          console.log("stdout: " + stdout);
          console.log("stderr: " + stderr);
      } else {
        console.log(err);
      }
  });
});

Node.jsサーバ用のjsファイルでは、Realtime Databaseとリアルタイムに同期させ、各家電の状態を示す値を監視させます。 値が変わったことをトリガーに赤外線発信をするirrp.pyを実行させています。 例えば下記の

"python3 /home/pi/webcon-node/irrp.py -p -g17 -f /home/pi/webcon-node/codes coolairOn"

のコマンドは、「codesファイル内のcoolairOnというキーの赤外線の値をGPIOの17ピン(出力用)から発信する」という意味を示しています。 操作したい家電によってソースは適宜変えて下さい。

さて、以上でNode.jsサーバが出来ましたので、テストしてみます。

$ node /home/pi/webcon-node/main.js

上記コマンドでNode.jsサーバを立ち上げた状態で、FirebaseコンソールからRealtimeDatabaseの値を変えてみて、うまく家電が動けばOKです。 動作しない場合、ソースコード以外に電子回路の組み方を間違っていないか確認すると解決するかもしれません。 実際、電子回路は素人なので私もよく間違えました…。

Node.jsサーバを常駐させる

毎回ブート時に手動でNode.jsサーバを起動するのは現実的ではありませんので、常駐させます。 npmパッケージであるforeverを使います。 foreverはnodeアプリがエラーで落ちたりした場合に自動的に再起動をしてくれます。

$ sudo npm install -g forever

ブート時にforeverでNode.jsサーバを起動し常駐させます。 rc.localからコードを実行する際は、フルパス指定しないと正しく動作しないので注意。

$ sudo nano /etc/rc.local
# rc.local
/usr/bin/forever start -o /home/pi/webcon-node/stdout.log -e /home/pi/webcon-node/stderr.log /home/pi/webcon-node/main.js

ここまでで赤外線発信をするサーバを作ることができました。 次の記事はもっと人間が気楽にRealtime Databaseを操作できるよう、webアプリを作っていきます! 一息ついて次に行きましょう。

tm-progapp.hatenablog.com