Raspberry Pi 3 & BMP180からJavaで気圧・温度を読み取る

#はじめに
Raspberry Pi 3 を使って、気圧・温度センサー BMP180 から気圧および温度情報を読み取ります。また海面気圧を指定すると、測定した気圧をもとに測定地点の高度を算出します。プログラミング言語は Java を使用しています。

|BMP180|
|:-:|
|![](https://qiita-image-store.s3.amazonaws.com/0/242156/6a7c4f5b-69c4-8d49-1ee9-c808e6166f25.jpeg)|

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

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

またこのセンサーは I2C で通信を行うため、Raspberry Pi の I2C 通信機能を有効にしておきます（デフォルトでは無効になっています）。

Raspbian の GUI から有効にする場合は、タスクバーの「Menu」ボタンから「設定」～「Raspberry Pi の設定」で設定パネルを開き、「インターフェイス」タブを選んで「I2C」を有効にします。コマンドラインから有効にする場合は、sudo raspi-config コマンドを入力し、「Interfacing Options」～「I2C」と選んで、最後に「Yes」を選択します。

設定した後は、念のためにリブートしておきます。

Raspberry Pi と BMP180 は以下のように接続しました。

![BMP180_ブレッドボード.png](https://qiita-image-store.s3.amazonaws.com/0/242156/5875ae6d-56cc-10b2-569e-11c3ef51582b.png)

#センサーの動作モード
このセンサーは、気圧を読み取る際に4つのモードのいずれかを指定して読み取ります。指定されるモードによって、サンプリング数（精度）と消費電力などが異なってきます。このモードは気圧の読み取り時のみに有効で、温度の読み取りに対しては無関係です。
![BMP180_Datasheet.png](https://qiita-image-store.s3.amazonaws.com/0/242156/497b32bc-9a81-cec9-44e6-8782c7bd2ca3.png)

（参考）[BMP180 データシート](https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf)

#プログラム
##構成
プログラムは、以下の2つのファイルから構成されています。
BMP180Demo.java
BMP180.java

センサーのアドレスはデフォルトの 77H を想定しています。センサーを接続した後以下のコマンドを入力し、77H 以外のところで認識されている場合は、BMP180.java の中で定義している定数 I2C_ADRESS にその値を設定してください。

```
pi@raspberrypi:~ $ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- 77
pi@raspberrypi:~ $
```

##ソースコード

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

public class BMP180Demo {
    public static void main(String[] args) throws Exception {
        BMP180 bmp180 = new BMP180();                       // use I2C bus 1, standard mode
        // BMP180 bmp180 = new BMP180(BMP180.ULTRAHIGHRES);   // use I2C bus 1, ultra high resolution mode
        // BMP180 bmp180 = new BMP180(BMP180.HIGHRES);        // use I2C bus 1, high resolution mode
        // BMP180 bmp180 = new BMP180(BMP180.STANDARD);       // use I2C bus 1, standared mode
        // BMP180 bmp180 = new BMP180(BMP180.ULTRALOWPOWER);  // use I2C bus 1, ultra low power mode

        while (true) {
            System.out.println("Last valid input: " + new Date());

            double temperature = bmp180.readTemperature();
            System.out.printf("Temperature: %.2f C (%.1f F)\n",
                    temperature, BMP180.convertToFahrenheit(temperature));

            double pressure = bmp180.readPressure();
            System.out.printf("Pressure   : %.2f hPa\n", pressure);

            // bmp180.setStandardSeaLevelPressure(pressure); // specify sea level pressure in hPa
            System.out.printf("Altitude   : %.2f m\n\n", bmp180.readAltitude());

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

```java:BMP180.java
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import com.pi4j.io.i2c.I2CBus;
import com.pi4j.io.i2c.I2CDevice;
import com.pi4j.io.i2c.I2CFactory;
import com.pi4j.io.i2c.I2CFactory.UnsupportedBusNumberException;

public class BMP180 {
    // Hardware pressure sampling accuracy modes
    public static final int ULTRALOWPOWER = 0;
    public static final int STANDARD      = 1;
    public static final int HIGHRES       = 2;
    public static final int ULTRAHIGHRES  = 3;

    private int mode;

    // Registers
    private static final int CAL_AC1   = 0xAA;
    private static final int CTRL_MEAS = 0xF4;
    private static final int OUT_MSB   = 0xF6;

    // Commands
    private static final byte CMD_READTEMP     = 0x2E;
    private static final byte CMD_READPRESSURE = 0x34;

    private static final int I2C_BUS     = I2CBus.BUS_1;
    private static final int I2C_ADDRESS = 0x77;
    private I2CDevice device;

    private int AC1;
    private int AC2;
    private int AC3;
    private int AC4;
    private int AC5;
    private int AC6;
    private int B1 ;
    private int B2 ;
    private int MB ;
    private int MC ;
    private int MD ;

    private double standardSeaLevelPressure = 1013.89; // avarage sea level pressure in Tokyo

    public BMP180(int i2cBus, int i2cAddress, int mode) throws UnsupportedBusNumberException, IOException {
        // Create I2C bus
        I2CBus bus = I2CFactory.getInstance(i2cBus);

        // Get I2C device
        device = bus.getDevice(i2cAddress);

        // Calibration Coefficients stored in EEPROM of the device
        // Read 22 bytes of data from address 0xAA(170)
        byte[] data = new byte[22];
        device.read(CAL_AC1, data, 0, data.length);

        // Convert the data
        AC1 = INT (data[ 0], data[ 1]);
        AC2 = INT (data[ 2], data[ 3]);
        AC3 = INT (data[ 4], data[ 5]);
        AC4 = UINT(data[ 6], data[ 7]);
        AC5 = UINT(data[ 8], data[ 9]);
        AC6 = UINT(data[10], data[11]);
        B1  = INT (data[12], data[13]);
        B2  = INT (data[14], data[15]);
        MB  = INT (data[16], data[17]);
        MC  = INT (data[18], data[19]);
        MD  = INT (data[20], data[21]);

        this.mode = mode;
    }

    public BMP180(int mode) throws UnsupportedBusNumberException, IOException {
        this(I2C_BUS, I2C_ADDRESS, mode);
    }

    public BMP180() throws UnsupportedBusNumberException, IOException {
        this(BMP180.STANDARD);
    }

    private int readAndCalcB5() throws IOException, InterruptedException {
        // Select measurement control register
        // Enable temperature measurement
        device.write(CTRL_MEAS, CMD_READTEMP);
        TimeUnit.MILLISECONDS.sleep(5);

        // Read 2 bytes of data from address 0xF6(246)
        // temp msb, temp lsb
        byte[] data = new byte[2];
        device.read(OUT_MSB, data, 0, data.length);

        // Convert the data
        int UT = UINT(data[0], data[1]);

        // Callibration for Temperature
        int X1 = ((UT - AC6) * AC5) >> 15;
        int X2 = (MC << 11) / (X1 + MD);
        int B5 = X1 + X2;

        return B5;
    }

    public double readTemperature() throws IOException, InterruptedException {
        return ((readAndCalcB5() + 8) >> 4) / 10.0;
    }

    public static double convertToFahrenheit(double c) {
        return c * 1.8 + 32.0;
    }

    public double readPressure() throws IOException, InterruptedException {
        // Select measurement control register
        // Enable pressure measurement
        device.write(CTRL_MEAS, (byte)(CMD_READPRESSURE + (mode << 6)));
        switch (mode) {
        case ULTRALOWPOWER:
            TimeUnit.MILLISECONDS.sleep(5);
            break;
        case STANDARD:
            TimeUnit.MILLISECONDS.sleep(8);
            break;
        case HIGHRES:
            TimeUnit.MILLISECONDS.sleep(14);
            break;
        default:
            TimeUnit.MILLISECONDS.sleep(26); // ULTRAHIGHRES mode
            break;
        }

        // Read 3 bytes of data from address 0xF6(246)
        // pres msb1, pres msb, pres lsb
        byte[] data = new byte[3];
        device.read(OUT_MSB, data, 0, data.length);

        int UP = UINT(data[0], data[1], data[2]) >> (8 - mode);

        // Calibration for Pressure
        int B6 = readAndCalcB5() - 4000;
        int X1 = (B2 * (B6 * B6) >> 12) >> 11;
        int X2 = (AC2 * B6) >> 11;
        int X3 = X1 + X2;
        int B3 = (((AC1 * 4 + X3) << mode) + 2) / 4;

        X1 = (AC3 * B6) >> 13;
        X2 = (B1 * ((B6 * B6) >> 12)) >> 16;
        X3 = ((X1 + X2) + 2) >> 2;
        int B4 = (AC4 * (X3 + 32768)) >> 15;
        int B7 = (UP - B3) * (50000 >> mode);

        int p = B7 < 0x80000000 ? (B7 * 2) / B4 : (B7 / B4) * 2;

        X1 = (p >> 8) * (p >> 8);
        X1 = (X1 * 3038) >> 16;
        X2 = (-7357 * p) >> 16;
        p = p + ((X1 + X2 + 3791) >> 4);

        return p / 100.0;
    }

    public void setStandardSeaLevelPressure(double standardSeaLevelPressure) {
        this.standardSeaLevelPressure = standardSeaLevelPressure;
    }

    public double readAltitude() throws IOException, InterruptedException {
        // Calculates the altitude in meters
        double pressure = readPressure();
        return 44330.0 * (1.0 - Math.pow(pressure / standardSeaLevelPressure, 0.1903));
    }

    private int INT(byte msb, byte lsb) {
        int hi = msb & 0xFF;
        return ((hi > 127 ? hi - 256 : hi) << 8) + (lsb & 0xFF);
    }

    private int UINT(byte msb, byte lsb) {
        return ((msb & 0xFF) << 8) + (lsb & 0xFF);
    }

    private int UINT(byte msb, byte lsb, byte xlsb) {
        return ((msb & 0xFF) << 16) + UINT(lsb, xlsb);
    }
}
```

#コンパイルと実行
上の2つのファイルを適当なディレクトリに置いてコンパイルします。

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

コンパイルが終わったらルート権限で実行します。毎秒ごとに温度・気圧・高度のデータを読み出して表示します。

高度については、あらかじめ海面気圧を指定しておくことにより、計測地との気圧差で値を計算します。海面気圧が指定されていない場合、東京の年間平均の海面気圧がデフォルトとして使用されます。気圧が高い日などは、海面下に沈んだりします。

プログラムを終了する場合は Ctrl+C を入力してください。

```
pi@raspberrypi:~ $ sudo pi4j BMP180Demo
+ java -classpath '.:classes:*:classes:/opt/pi4j/lib/*' BMP180Demo
Last valid input: Sun May 13 20:29:44 JST 2018
Temperature: 23.70 C (74.7 F)
Pressure   : 1000.57 hPa
Altitude   : 110.92 m

Last valid input: Sun May 13 20:29:45 JST 2018
Temperature: 23.60 C (74.5 F)
Pressure   : 1000.73 hPa
Altitude   : 109.57 m

Last valid input: Sun May 13 20:29:46 JST 2018
Temperature: 23.60 C (74.5 F)
Pressure   : 1000.57 hPa
Altitude   : 110.75 m

Last valid input: Sun May 13 20:29:47 JST 2018
Temperature: 23.60 C (74.5 F)
Pressure   : 1000.69 hPa
Altitude   : 110.08 m

^Cpi@raspberrypi:~ $
```

#プログラム利用時の注意点など
実際にプログラムを利用する場合必要になるのは、BMP180.java ファイルのみです。インスタンスの作成方法と、利用できるメソッドは以下のとおりです。

##インスタンスの作成
クラス BMP180 のコンストラクタのパラメータは、以下の3通りの指定方法があります。

```
BMP180 bmp180 = new BMP180();                         // I2C バス 1、アドレス 0x77、Standard モードで動作

BMP180 bmp180 = new BMP180(BMP180.ULTRAHIGHRES);      // I2C バス 1、アドレス 0x77、Ultra High Resoluion モードで動作

BMP180 bmp180 = new BMP180(2, 0x01, BMP180.STANDARD); // I2C バス 2、アドレス 0x01、Standard モードで動作
```

モードを指定する場合は、以下のいずれかを指定します。

```
BMP180.ULTRALOWPOWER   // Ultra Low Power モード
BMP180.STANDARD        // Standard モード
BMP180.HIGHRES         // High Resolution モード
BMP180.ULTRAHIGHRES    // Ultra High Resolution モード
```

##メソッド
以下のメソッドが用意されています。

| メソッドと機能|
|:--|
|public double readTemperature() throws IOException, InterruptedException|
|<dl><dt><dd>センサーから温度を読み取り、その値を返します。単位は度（摂氏）です。</dd></dt></dl>|
|public static double convertToFahrenheit(double c)|
|<dl><dt><dd>パラメータに度（摂氏）の値を指定すると、度（華氏）に変換して返します。</dd></dt></dl>|
|public double readPressure() throws IOException, InterruptedException|
|<dl><dt><dd>センサーから気圧を読み取り、その値を返します。単位は hPa（ヘクトパスカル）です。</dd></dt></dl>|
|public void setStandardSeaLevelPressure(double standardSeaLevelPressure)|
|<dl><dt><dd>高度を測定する準備として、海面気圧を設定します。単位は hPa（ヘクトパスカル）で指定します。</dd></dt></dl>|
|public double readAltitude() throws IOException, InterruptedException|
|<dl><dt><dd>センサーから読み取った気圧とあらかじめ設定されている海面気圧をもとに、高度を返します。単位は m（メートル）です。</dd></dt></dl>|