HomeBridge入門: (2) node.jsでLチカサーバを作る

前回は、Raspberry Pi Zero WでLEDをチカチカ(通称Lチカ)させました。今回は、特定のURLにアクセスするとLチカする、Lチカサーバを作ります。 iPhoneのショートカットにURLアクセスを記述しておけば、HomePodに「Hey Siri, LEDをつけて」とお願いするとLチカします。

node.js環境を整備する

前回の記事の最後では、Raspberry Piの汎用入出力ポート(GPIO)に接続したLEDを、ファイルIO経由で点滅させました。各種のプログラミング言語用に、GPIOを制御するライブラリが用意されていますので、それを使えばもっと洗練されたプログラムが書けます。Raspberry Piでよく使われる言語はPythonのような気がしますが、ここではあえてnode.jsで動くJavaScriptでLチカします。node.jsでLチカする方法はあまり紹介されていないと思います。この後の回でインストールするHomebridgeがnode.js上で動きますので、それに合わせようというのがnode.jsを使う理由です。

そこで、まずはnode.jsをインストールします。

$ sudo apt install nodejs

次にnode.jsのパッケージ管理ツールnpmもインストールしておきます。

$ sudo apt install npm

node.jsでRaspberry PiのGPIOにアクセスするライブラリはいくつか用意されているようです。今回はrpi-gpioというパッケージを選びました。これ以外にも、node-rpioというパッケージも見つかりました。node-rpioは、昔ながらのAPIになっていて親しみやすい印象でした。それに対して、rpi-gpioはnode.js風のAPIという印象です。どうせならnode.jsの流儀にどっぷり浸かってみようということで、rpi-gpioを使いました。npmでインストールします。

npm install rpi-gpio

node.jsのタイマー機能

Lチカを実現する前に、node.jsのタイマー機能を学んでおくべきのようです。以下で学んだ内容によると、

node.jsは、サーバに次々にやってくるリクエストに効率良く応答することを目指しているので、プログラムが停滞する(動作がブロックされる)ことを嫌う傾向があるようです。なので、Lチカのような機能を実現するのに、無限ループの中に「LEDをオン、1秒遅延、LEDをオフ、また1秒遅延」というような、あからさまに遅延する安易なコードは書かないようです。タイマー機能でコントロールされた無駄のないコードを書くのが流儀のようです。コンビニの店員さんが、前のお客のお弁当を温めながら、次のお客さんの対応をするようなものですね。弁当がチンできたら知らせるから、その間に次のお客の対応をしていてください、という仕組みのようです。

そんなわけで、例えば1秒ごとに何か仕事をさせたいなら、

function intervalFunc() {
  console.log('Cant stop me now!');
}

setInterval(intervalFunc, 1000);

こんな書き方をするようです。また、1秒後に何か仕事をさせたいなら、

function myFunc() {
  console.log('Hi, again!');
}

console.log('See you later.');
setTimeout(myFunc, 1000);

というような書き方をするようです。一般に、コールバック関数を用意しておいて、実行すべきタイミングになったら実行するのが、node.jsの流儀だと理解しました。この例では、一旦setTimeoutを発行したら、1秒待つことなく次の処理に取り掛かれるわけです。

node.js風味のLチカ

rpi-gpioのAPIも、一部がコールバック関数方式になってます。使い方は、こちらで紹介されています。

例えば、GPIOを入力または出力に設定するsetup関数は、3個目の引数がコールバック関数です。setupの仕事が終わったところで、この関数を呼んでくれるという方式です。エラーがある時は、この関数の最初の引数で知らせてくれるようです。ということで、1秒ごとにLチカするプログラムは以下のようになります。呼ばれるたびにLEDのオン・オフを繰り返すloop関数を用意しています。次のstartBlinking関数では、loop関数を1秒間隔で繰り返すタイマー設定をしています。プログラムは、最初にGIOPをsetupして、その結果、startBlinking関数を呼び出します。

var gpio = require('rpi-gpio');
const LED_PIN = 40; // GPIO21のピン番号
const LED_DELAY = 1000;  // 1秒のLチカ

var ledIsOn = true; //LEDの点滅状態

function loop() {
	if(ledIsOn) { // LEDが点いていればLEDをoffに
		gpio.write(LED_PIN, false);
		ledIsOn = false;
	} else { // LEDが消えていればLEDをonにする
		gpio.write(LED_PIN, true);
		ledIsOn = true;
	}
}
function startBlinking(err) {
	if(err) throw err; //エラーがあれば引数に入っている
	setInterval(loop,LED_DELAY);
}

// LED_PINを出力に設定して、その後、startBlinkingを実行
gpio.setup(LED_PIN, gpio.DIR_OUT, startBlinking);

GPIOに出力する関数writeはすぐに終了するためか、コールバック方式にはなってません。今回は使用しませんが、read関数はコールバック方式です。

さらに、一度しか呼ばれない関数は、引数の場所に名無しの関数として定義することもあるようです。以下のような内容になります。上のプログラムと全く同じ内容なのですが、慣れないと読みにくいです。

var gpio = require('rpi-gpio'); 
const LED_PIN = 40; // GPIO21のピン番号 
const LED_DELAY = 1000; // 1秒のLチカ 

var ledIsOn = true; //LEDの点滅状態 

// LED_PINを出力に設定して、その後、startBlinkingを実行 
gpio.setup(LED_PIN, gpio.DIR_OUT, (err) => {
	if(err) throw err; //エラーがあれば引数に入っている 
	setInterval(()=>{
		if(ledIsOn) { // LEDが点いていればLEDをoffに 
			gpio.write(LED_PIN, false); 
			ledIsOn = false; 
		} else { // LEDが消えていればLEDをonにする 
			gpio.write(LED_PIN, true); 
			ledIsOn = true; 
		}
	},LED_DELAY); 
});

httpに応答するLチカサーバ

以前の記事でも紹介しましたが、node.jsを使えば特定のURLに対応して応答を返すサーバを簡単に書けます。下は、URLに対してテキストを返すだけのhttpサーバです。最初にcreateServerメソッドでサーバを定義して、次にlistenでサーバを起動しています。

var http = require('http');

var server = http.createServer(function(req,res){
	res.writeHead(200, {'Content-Type' : 'text/plain'});
	var msg;
	switch (req.url) {
		case '/LED_on':
			msg = 'Turned on.';
			break;
		case '/LED_off':
			msg = 'Turned off.';
			break;
		case '/LED_on_off':
			msg = 'Turned on and off.';
			break;
		default: 
			msg = 'Command not found.';
			break;
	}
	res.write(msg + '\n');
        res.end();
});

server.listen(8080);

この結果、別のマシンからこのアドレスを指定すると、以下のような応答が得られます。

% curl http://192.168.xxx.xxx:8080/LED_on    
Turned on.
% curl http://192.168.xxx.xxx:8080/LED_off
Turned off.
% curl http://192.168.xxx.xxx:8080/LED_on_off
Turned on and off.
% curl http://192.168.xxx.xxx:8080/hogehoge  
Command not found.

このプログラムと、先ほどのGPIOアクセスのプログラムを組み合わせれば、httpのリクエストに対してLEDを点滅させるLチカサーバを、以下のようなプログラムで実現できます。最初にサーバを定義して、次にGPIOをsetupして、そのコールバック関数でサーバを起動しています。

var http = require('http');
var gpio = require('rpi-gpio');
const GPIO_PIN = 40;
const DELAY = 1000; //delay for on
const SERVER_PORT = 8080;

var server = http.createServer(function(req,res){
	res.writeHead(200, {'Content-Type' : 'text/plain'});
	var msg;
	switch (req.url) {
		case '/LED_on':
			gpio.write(GPIO_PIN, true);
			msg = 'Turned on.';
			break;
		case '/LED_off':
			gpio.write(GPIO_PIN, false);
			msg = 'Turned off.';
			break;
		case '/LED_on_off':
			gpio.write(GPIO_PIN, true);
			setTimeout(()=>{gpio.write(GPIO_PIN,false);},DELAY);
			msg = 'Turned on and off.';
			break;
		default: 
			msg = 'command not found.';
			break;
		}
	res.write(msg + '\n');
        res.end();
});

gpio.setup(GPIO_PIN, gpio.DIR_OUT, (err)=>{
	if(err) throw err;
	server.listen(SERVER_PORT);
});

Lチカのために1秒間LEDを点灯する部分もコールバック関数方式で、ブロッキングしないで終了するようにプログラムされてます。なので、すぐに次のhttpリクエストに対応できます。例えば、1秒LEDが点灯している間に、LED_offのリクエストを受け取ると、1秒を待たずにLEDが消灯します。

サービスを自動起動する

以上のように、nodeコマンドでこのサーバプログラムを起動すれば、以後はhttpリクエストに応じてLEDが点灯します。でもこのままでは、Raspberry Piを起動するたびに、サーバ(サービス)を手作業で起動する必要があります。そこで、Linuxが起動したときに自動的にプログラムを起動するよう設定しておきます。こちらに説明がありました。

自動起動の方法はいくつかあります。

  1. /etc/rc.local
  2. /etc/init.d
  3. crontab @reboot
  4. systemd

rc.localやinit.dは、起動コマンドを指定のファイルに書いたり、指定のディレクトリに入れておく方法です。crontabに書いてしまう方法もあります。これらは昔からある馴染みのある方法です。でも、今時なモダンな正しい方法は、systemdを使う方法のようです。後でインストールするHomebridgeもこの方法で起動しています。なのでsystemdを使ってみることにしました。

systemdで起動するよう設定するには、

/etc/systemd/system

にサービスを設定するファイルを作ります。今回作ったサービスは、httpでアクセスしてスイッチを入れるものなので、httpSwitchという名前のサービスにします。これを設定するファイルを、httpSwitch.serviceという名前にして、/etc/systemd/system/に置きます。パーミッションが制約されているのでsudoでエディタを起動します。

$ sudo vi /etc/systemd/system/httpSwitch.service

このファイルの内容を以下のよう書きます。

[Unit]
Description=httpSwitch
After=syslog.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/js
ExecStart=/usr/bin/node /home/pi/js/httpSwitch.js
Restart=always
KillMode=process

[Install]
WantedBy=multi-user.target

ほとんどが一般的なデフォルト値です。Descriptionがサービスの名前、Afterはそれが動いてから起動するような意味です。Userは実行者、WorkingDirectoryは作業場所、Restartはなんらかの理由で落とされたら再起動する設定、などのようです。重要なのはExecStartで、ここに起動のコマンドを書いておきます。

このファイルを用意したら、以下のコマンドでロードしておきます。プログラムを書き換えた後などもdaemon-reloadし直しておきます。

$ sudo systemctl daemon-reload

以上の準備が終わったら、systemctlコマンドで以下のような操作ができます。まずはstartでいつでもサービスを起動することができます。

$ sudo systemctl start httpSwitch

stopでサービスを停止できます。

$ sudo systemctl stop httpSwitch

enableで自動起動を設定できます。以後、電源が入ると自動的に起動します。

$ sudo systemctl enable httpSwitch

disableで自動起動を停止できます。

$ sudo systemctl disable httpSwitch

「Hey Siri, LEDをつけて」でLチカする

あとはこのアドレスにアクセスするショートカットをiPhone上に作れば、iPhoneから操作できますし、iPhoneと接続したHomePodのSiriに呼びかけて、音声でLチカをさせることが可能になります。作り方は、こちらの記事で紹介した通りです。

ここまで準備が長かったですが、次回はようやくHomebridgeを使ってのHomeKitアクセサリ作りをします。

まとめ

Raspberry Pi Zero Wの上に、httpアクセスがあるとLEDが点滅するLチカサーバを構築しました。iPhoneのショートカット経由で、Hei SiriでLチカができるようになりました。次回はいよいよHomebridgeを使います。