Raspberry Pi 3 & DHT11からJavaで温度・湿度を読み取る

#はじめに
Raspberry Pi 3 を使って、安価で入手可能な温湿度センサー DHT11 から温度および湿度の情報を読み取ります。プログラミング言語は Java を使用しています。

|DHT11|
|:-:|
|![M-07003.jpg](https://qiita-image-store.s3.amazonaws.com/0/242156/eb0136bb-469f-bfa4-dd68-130a78b033b2.jpeg)|

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

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

今回 Raspberry Pi と DHT11 は以下のように接続しました。データシートには、VDD と Data パスの間に 5KΩ の抵抗が記載されていましたが、手持ちになかったので 10KΩ で代用しました（ここはあまり詳しくないので、とりあえず良しとします・・・）。

（参考）[DHT11のデータシート](http://osoyoo.com/driver/DHT11-datasheet.pdf "DHT11Humidity&Temperature Sensor")

**回路図**
![DHT11.png](https://qiita-image-store.s3.amazonaws.com/0/242156/3d1fb6c2-8c33-bfdb-5cbc-8bf7998a5a5d.png)
**実態配線図**
![DHT11_ブレッドボード.png](https://qiita-image-store.s3.amazonaws.com/0/242156/25505f69-1a9a-6a20-d9f5-c27f255e7a2b.png)

#プログラム
プログラムは以下のリンク先にある Python のプログラムを参照して、Java に書き換えてみたものです（少し変更を入れています）。

> [DHT11 Python library](https://github.com/szazo/DHT11_Python.git "DHT11 Python library")

プログラムは、以下の3つのファイルから構成されています。

- DHT11Demo.java
- DHT11.java
- DHT11Result.java

メインとなるクラス DHT11 のインスタンス作成時に、コンストラクタに対して DHT11 の Data ピンが接続されている Raspberry Pi のピンの **GPIO 番号**を指定します。例えば Raspberry Pi のピン番号 8 に接続されている場合は、対応する GPIO 番号である 15 を指定します。

（参考）[Raspberry Pi 3 のピン割り当て](http://pi4j.com/pins/model-3b-rev1.html)

```java:DHT11Demo.java
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class DHT11Demo {
    public static void main(String[] args) throws InterruptedException {
        DHT11 dht11 = new DHT11(15); // use GPIO pin 15

        while (true) {
            DHT11Result result = dht11.read();

            if (result.isValid()) {
                System.out.println("Last valid input: " + new Date());
                System.out.printf("Temperature: %.1f C\n" , result.getTemperature());
                System.out.printf("Humidity:    %.1f %%\n", result.getHumidity());
            }

            TimeUnit.SECONDS.sleep(2);
        }
    }
}
```

```java:DHT11.java
import java.util.ArrayList;
import java.util.List;

import com.pi4j.wiringpi.Gpio;
import com.pi4j.wiringpi.GpioUtil;

public class DHT11 {
    // DHT11 sensor reader class for Raspberry Pi

    private int pin; // GPIO pin number
    private List<Integer> data    = new ArrayList<>(10000);
    private List<Integer> lengths = new ArrayList<>(40); // will contain the lengths of data pull up periods

    public DHT11(int pin) {
        // setup wiringPi
        if (Gpio.wiringPiSetup() != 0) {
            throw new RuntimeException("Initialization of the GPIO has failed.");
        }
        this.pin = pin;
        GpioUtil.export(pin, GpioUtil.DIRECTION_OUT);
    }

    public DHT11Result read() {
        data.clear();
        lengths.clear();

        // change to output, then send start signal
        Gpio.pinMode(pin, Gpio.OUTPUT);

        Gpio.digitalWrite(pin, Gpio.LOW);
        Gpio.delay(18); // ms

        Gpio.digitalWrite(pin, Gpio.HIGH);

        // change to input
        Gpio.pinMode(pin, Gpio.INPUT);

        // collect data into a list
        collectInput();

        // parse lengths of all data pull up periods
        parseDataPullUpLengths();

        // if bit count mismatch, return error (4 byte data + 1 byte checksum)
        if (lengths.size() != 40)
            return new DHT11Result(DHT11Result.ERR_MISSING_DATA, 0.0, 0.0, data.size(), lengths.size());

        // calculate bits from lengths of the pull up periods
        boolean[] bits = calculateBits();

        // we have the bits, calculate bytes
        byte[] bytes = bitsToBytes(bits);

        // calculate checksum and check
        if (bytes[4] != calculateChecksum(bytes))
            return new DHT11Result(DHT11Result.ERR_CRC, 0.0, 0.0, data.size(), lengths.size());

        // ok, we have valid data, return it
        return new DHT11Result(DHT11Result.ERR_NO_ERROR,
                Double.parseDouble(bytes[2] + "." + bytes[3]),
                Double.parseDouble(bytes[0] + "." + bytes[1]),
                data.size(), lengths.size());
    }

    // this is used to determine where is the end of the data
    private final int MAX_UNCHANGED_COUNT = 500;

    private void collectInput() {
        // collect the data while unchanged found
        int unchangedCount = 0;

        int last = -1;
        while (true) {
            int current = Gpio.digitalRead(pin);
            data.add(current);

            if (last != current) {
                unchangedCount = 0;
                last = current;
            } else {
                if (++unchangedCount > MAX_UNCHANGED_COUNT) break;
            }
        }
    }

    protected enum SignalTransition {
        STATE_INIT_PULL_DOWN      ,
        STATE_INIT_PULL_UP        ,
        STATE_DATA_FIRST_PULL_DOWN,
        STATE_DATA_PULL_UP        ,
        STATE_DATA_PULL_DOWN
    };

    private void parseDataPullUpLengths() {
        SignalTransition state = SignalTransition.STATE_INIT_PULL_DOWN;

        int currentLength = 0; // will contain the length of the previous period

        for (int current : data) {
            currentLength++;

            switch (state) {
            case STATE_INIT_PULL_DOWN:
                if (current == Gpio.LOW) {
                    // ok, we got the initial pull down
                    state = SignalTransition.STATE_INIT_PULL_UP;
                }
                break;

            case STATE_INIT_PULL_UP:
                if (current == Gpio.HIGH) {
                    // ok, we got the initial pull up
                    state = SignalTransition.STATE_DATA_FIRST_PULL_DOWN;
                }
                break;

            case STATE_DATA_FIRST_PULL_DOWN:
                if (current == Gpio.LOW) {
                    // we have the initial pull down, the next will be the data pull up
                    state = SignalTransition.STATE_DATA_PULL_UP;
                }
                break;

            case STATE_DATA_PULL_UP:
                if (current == Gpio.HIGH) {
                    // data pulled up, the length of this pull up will determine whether it is 0 or 1
                    currentLength = 0;
                    state = SignalTransition.STATE_DATA_PULL_DOWN;
                }
                break;

            case STATE_DATA_PULL_DOWN:
                if (current == Gpio.LOW) {
                    // pulled down, we store the length of the previous pull up period
                    lengths.add(currentLength);
                    state = SignalTransition.STATE_DATA_PULL_UP;
                }
                break;
            }
        }
    }

    private boolean[] calculateBits() {
        boolean[] bits = new boolean[40];

        int longestPullUp  = lengths.stream().mapToInt(Integer::intValue).max().getAsInt();
        int shortestPullUp = lengths.stream().mapToInt(Integer::intValue).min().getAsInt();

        // use the halfway to determine whether the period it is long or short
        int halfway = shortestPullUp + (longestPullUp - shortestPullUp) / 2;

        int i = 0;
        for (int length : lengths) bits[i++] = length > halfway;

        return bits;
    }

    private byte[] bitsToBytes(boolean[] bits) {
        byte[] bytes = new byte[5];
        byte   value = 0;

        for (int i = 0; i < bits.length; i ++) {
            value <<= 1;
            if (bits[i]) value |= 1;

            if (i % 8 == 7) {
                bytes[i / 8] = value;
                value = 0;
            }
        }
        return bytes;
    }

    private byte calculateChecksum(byte[] bytes) {
        return (byte)(bytes[0] + bytes[1] + bytes[2] + bytes[3]);
    }
}
```

```java:DHT11Result.java
public class DHT11Result {
    // DHT11 sensor result returned by DHT11.read() method

    public static final int ERR_NO_ERROR     = 0;
    public static final int ERR_MISSING_DATA = 1;
    public static final int ERR_CRC          = 2;

    private int errorCode = ERR_NO_ERROR;
    private double temperature;
    private double humidity;
    private int listElements;                // for debug
    private int detectedBits;                // for debug

    DHT11Result(int errorCode, double temperature, double humidity, int listElements, int detectedBits) {
        this.errorCode   = errorCode;
        this.temperature = temperature;
        this.humidity    = humidity;

        this.listElements = listElements;
        this.detectedBits = detectedBits;
    }

    public boolean isValid() {
        return errorCode == ERR_NO_ERROR;
    }
    public int getErrorCode() {
        return errorCode;
    }
    public double getTemperature() {
        return temperature;
    }
    public double getHumidity() {
        return humidity;
    }
    double getListElements() {
        return listElements;
    }
    double getDetectedBits() {
        return detectedBits;
    }
}
```
#コンパイルと実行
上の3つのファイルを適当なディレクトリに置いてコンパイルします。

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

コンパイルが終わったらルート権限で実行します。2秒ごとに温度・湿度のデータを読み出して表示します。終了する場合は Ctrl+C を入力してください。

```
pi@raspberrypi:~ $ sudo pi4j DHT11Demo
+ java -classpath '.:classes:*:classes:/opt/pi4j/lib/*' DHT11Demo
Last valid input: Sun May 13 11:02:56 JST 2018
Temperature: 26.6 C
Humidity:    52.0 %
Last valid input: Sun May 13 11:02:58 JST 2018
Temperature: 26.7 C
Humidity:    52.0 %
Last valid input: Sun May 13 11:03:00 JST 2018
Temperature: 26.7 C
Humidity:    52.0 %
Last valid input: Sun May 13 11:03:02 JST 2018
Temperature: 26.7 C
Humidity:    52.0 %
Last valid input: Sun May 13 11:03:06 JST 2018
Temperature: 26.8 C
Humidity:    51.0 %
^Cpi@raspberrypi:~ $
```

DHT11 はタイムクリティカルな通信を行うため、マルチタスク OS 上で動く Java からの読み取りは比較的失敗することが多く、特に起動時は数回続けて失敗することが多いです。実際に使用する場合はサンプルコード（DHT11Demo.java）のように DHT11Result クラスの isValid() メソッドで読み取り結果を確認し、成功するまでリトライしてください（メソッドが false を返す場合は、読み取りに失敗しています）。