人感センサーHC-SR501が検知するモーションをRaspberry Pi 3 & Javaで処理する

#はじめに

Raspberry Pi 3 を使って、人感センサー HC-SR501 のモーション検知状態を読み取ります。プログラミング言語は Java を使用しています。

|HC-SR501|
|:-:|
|![HC-SR501.png](https://qiita-image-store.s3.amazonaws.com/0/242156/51ad1e4e-aae8-6e23-cb7e-1b8511613cd4.png)|

この人感センサーは赤外線の動きをセンサーで感知し、変化があるとアウトプットがハイレベル（3.3V）になり、変化がなくなるとローレベル（0V）になるものです。

モジュールの側面にはオレンジ色のふたつの半固定抵抗がついていて、向かって左側を時計回りに回すと、検知後の遅延時間を約5秒から300秒の範囲で、右側を時計回りに回すと、検知距離を約3メートルから7メートルの範囲で調整することができます。

（参考）[HC-SR501のデータシート](https://www.mpja.com/download/31227sc.pdf)

#準備

Raspberry Pi の GPIO を Java から制御するためには Pi4J を使用します。Pi4J の環境については、以下の記事の「Pi4J のインストール」をご参照ください。

>[Raspberry Piのセンサー情報をグラフ化し、Webブラウザーで確認できる環境を準備する](https://qiita.com/uchino-tama/items/bdc5e546822decd9bbef)

Pi4J をインストールすると、ディレクトリ /opt/pi4j/examples 配下に、種々のサンプルプログラムが一緒にコピーされてきます。モーション検知状態の確認には、このサンプルの中のソースコードを利用することができます。

#サンプルプログラム

モーションの検知状態は、アウトプットのレベルがハイかローかを見るだけなので、スイッチと同じプログラムロジックが利用できます。

GPIO ピンのレベルの状態に従って何かの処理を行いたい場合、プログラム内でループを回して定期的にピンのレベルをチェックする方法もありますが、ピンの状態が変化したタイミングで処理を実行する、いわゆるイベント・ドリブンのプログラムを書くことができます。

/opt/pi4j/examples の中の ListenGpioExample.java が、その処理を行うソースコードです。

プログラムを実際に試してみる場合は、Raspberry Pi と HC-SR501 を以下のように接続します。

![HC-SR501](https://qiita-image-store.s3.amazonaws.com/0/242156/fc8a30a2-2cd0-d8ed-fdac-4f01949b7158.png)

プログラムでは、インタフェース GpioPinListenerDigital を実装し、リスナーとなるhandleGpioPinDigitalStateChangeEvent() メソッドをオーバライドした匿名クラスのインスタンスを、GPIO ピンのインスタンスに登録しています。

```java:ListenGpioExample.javaの一部（コメントを書き換えています）

// GpioControllerインスタンスを作成
final GpioController gpio = GpioFactory.getInstance();

// 対象となるピンを入力ピンとして指定
final GpioPinDigitalInput myButton = gpio.provisionDigitalInputPin(RaspiPin.GPIO_02, PinPullResistance.PULL_DOWN);

// ピンの状態が変化したときに呼び出されるリスナーを登録
myButton.addListener(new GpioPinListenerDigital() {
    @Override
    public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
        // ピンの状態をコンソールに表示
        System.out.println(" --> GPIO PIN STATE CHANGE: " + event.getPin() + " = " + event.getState());
    }
});
```

これにより、ピンの状態が変化するたびに上記の処理が呼び出されるようになります。

ListenGpioExample.java を適当なディレクトリにコピーし、以下のコマンドでコンパイルすれば動きを確認することができます。

```
pi@raspberrypi:~ $ pi4j -c ListenGpioExample.java
--------------------------------------------
Pi4J - Compiling: ListenGpioExample.java
--------------------------------------------
+ javac -classpath '.:classes:*:classes:/opt/pi4j/lib/*' -d . ListenGpioExample.java
pi@raspberrypi:~ $
```

コンパイルが終わったらルート権限で実行します。終了する場合は Ctrl+C を入力します。

```
pi@raspberrypi:~ $ sudo pi4j ListenGpioExample
+ java -classpath '.:classes:*:classes:/opt/pi4j/lib/*' ListenGpioExample
<--Pi4J--> GPIO Listen Example ... started.
 ... complete the GPIO #02 circuit and see the listener feedback here in the console.
 --> GPIO PIN STATE CHANGE: "GPIO 2" <GPIO 2> = HIGH
 --> GPIO PIN STATE CHANGE: "GPIO 2" <GPIO 2> = LOW
 --> GPIO PIN STATE CHANGE: "GPIO 2" <GPIO 2> = HIGH
 --> GPIO PIN STATE CHANGE: "GPIO 2" <GPIO 2> = LOW
^Cpi@raspberrypi:~ $
```

状態の変化を即座に読み取れるように、最初は遅延時間のつまみを半時計方向いっぱいに回した状態でプログラムを実行すると良いでしょう。

#デモプログラム

上記の人感センサーの他に I2C インタフェースを持った 1602 LCD ディスプレイとタクトスイッチを組み合わせたデモプログラムを作成してみました。

|I2C 1602 LCD|タクトスイッチ|
|:-:|:-:|
|![I2C 1602 LCD](https://qiita-image-store.s3.amazonaws.com/0/242156/0e55d17a-9d38-d5a6-349f-aa8843ed51d1.jpeg)|![タクトスイッチ](https://qiita-image-store.s3.amazonaws.com/0/242156/1ab819da-dedb-2971-683b-e87996438673.png)|

このプログラムは以下の動作をします。

・プログラムをスタートさせると LCD 上に日付と時刻を表示する
・タクトスイッチを押すたびに時刻表示が12時間表記と24時間表記に切り替わる
・人が近づくと LCD のバックライトが点灯し、動きがなくなると消灯する

1602 LCD を使用するために　Raspberry Pi の I2C 通信機能を有効にしておきます。有効にする方法、および 1602 LCD を使用するためのプログラムの詳細は以下の記事をご参照ください。

>[Raspberry Pi 3 & JavaでI2C 1602 LCDに文字を表示する](https://qiita.com/uchino-tama/items/9994f1164366dc94729d)

Raspberry Pi と各パーツは、以下のように接続しました。

![HC-SR501 Demo](https://qiita-image-store.s3.amazonaws.com/0/242156/78ca0275-98ed-9dfb-f3c4-2d6a18503b74.png)

実際にデモプログラムを試される場合は、1602 LCD に割り当てられているアドレスに合わせて変数 i2cAddress の値を修正してください（アドレスの確認方法も上記の記事の中で触れています）。

スイッチも人感センサーも、上で述べたように処理構造は全く同じです。

```java:HC_SR501Demo.java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

import com.pi4j.component.lcd.LCDTextAlignment;
import com.pi4j.component.lcd.impl.I2CLcdDisplay;
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalInput;
import com.pi4j.io.gpio.Pin;
//import com.pi4j.io.gpio.PinPullResistance;
import com.pi4j.io.gpio.RaspiPin;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;

public class HC_SR501Demo {
    // スイッチが接続されたピンの指定
    private static final Pin gpioTactSwitch = RaspiPin.GPIO_07;
    // 人感センサーが接続されたピンの指定
    private static final Pin gpioPirSensor = RaspiPin.GPIO_02;

    // I2C 1602 LCDモジュールが接続されたI2Cバス番号とアドレスの指定
    private static final int i2cBus = 1;
    private static final int i2cAddress = 0x27;

    // 表示形式の定義
    private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("y/MM/dd eee.", Locale.ENGLISH);
    private static final DateTimeFormatter timeFormatter12 = DateTimeFormatter.ofPattern("h:mm:ss a", Locale.ENGLISH);
    private static final DateTimeFormatter timeFormatter24 = DateTimeFormatter.ofPattern("HH:mm:ss");

    // 現在の時刻表示形式（true：12時間　false:24時間）
    private static boolean timeFormat = true;

    public static void main(String[] args) throws Exception {
        // I2C 1602 LCDモジュール用オブジェクトの作成
        final I2CLcdDisplay lcd = new I2CLcdDisplay(2, 16, i2cBus, i2cAddress, 3, 0, 1, 2, 7, 6, 5, 4);

        // GpioControllerインスタンスの作成
        final GpioController gpio = GpioFactory.getInstance();

        // スイッチが接続されたピンを入力ピンとして設定
        final GpioPinDigitalInput pinTactSwitch = gpio.provisionDigitalInputPin(gpioTactSwitch);
        //final GpioPinDigitalInput pinTactSwitch = gpio.provisionDigitalInputPin(gpioTactSwitch, PinPullResistance.PULL_DOWN);

        // スイッチが接続されたピンの状態が変化したときに呼び出されるリスナーを登録
        pinTactSwitch.addListener(new GpioPinListenerDigital() {
            @Override
            public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
                // スイッチが押される（プルアップ）たびに時刻の表示形式を切り替える
                if (event.getState().isHigh()) timeFormat = !timeFormat;
            }
        });

        // 人感センサーが接続されたピンを入力ピンとして設定
        final GpioPinDigitalInput pinPirSensor = gpio.provisionDigitalInputPin(gpioPirSensor);
        // LCDバックライトの初期状態を設定
        lcd.setBacklight(pinPirSensor.isHigh());

        // 人感センサーが接続されたピンの状態が変化したときに呼び出されるリスナーを登録
        pinPirSensor.addListener(new GpioPinListenerDigital() {
            @Override
            public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
                // モーション検知（プルアップ）時にLCDバックライトをオン、未検知（プルダウン）時にオフにする
                lcd.setBacklight(event.getState().isHigh());
            }
        });

        while (true) {
            // 現在日時の取得
            LocalDateTime dateTime = LocalDateTime.now();

            // LCDの1行目に日付、2行目に時刻をそれぞれ中央寄せで表示
            lcd.writeln(0, dateTime.format(dateFormatter), LCDTextAlignment.ALIGN_CENTER);
            lcd.writeln(1, dateTime.format(timeFormat ? timeFormatter12 : timeFormatter24), LCDTextAlignment.ALIGN_CENTER);
            Thread.sleep(100);
        }
    }
}
```

このプログラムを実行するには、上のファイルを適当なディレクトリに置いてコンパイルします。

```
pi@raspberrypi:~ $ pi4j -c HC_SR501Demo.java
--------------------------------------------
Pi4J - Compiling: HC_SR501Demo.java
--------------------------------------------
+ javac -classpath '.:classes:*:classes:/opt/pi4j/lib/*' -d . HC_SR501Demo.java
pi@raspberrypi:~ $
```

コンパイルが終わったらルート権限で実行します。終了する場合は Ctrl+C を入力します。

```
pi@raspberrypi:~ $ sudo pi4j HC_SR501Demo
+ java -classpath '.:classes:*:classes:/opt/pi4j/lib/*' HC_SR501Demo
^Cpi@raspberrypi:~ $
```

バックライトがチカチカ過敏に反応してしまう場合は、遅延時間のつまみをほんの少しだけ時計方向に回して調整すると、よい感じに落ち着きます。