Spring × Ras Piでセンサから取得したデータを表示する #02Ras Pi編

# 前回を行ったこと
Docker上にデータベース構築とSpringの構築

# 今回行うこと
Ras Pi側でセンサからのデータを取得してdocker内のデータベースにデータを登録する。

## Ras Piにセンサを搭載する
搭載するとこんな感じ
![IMG_0594.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3473187/5fd4f117-8c35-0204-1873-b9581dfa9742.jpeg)


<table>
  <caption>RasPiピン配置</caption>
　<thead>
    <tr>
      <th>Raspberry Pi</th> <th>説明</th>
    </tr>
  </thead>
  <tr>
    <td> 3V3 power(PIN1) </td> <td>電源</td>
  </tr>
  <tr>
    <td> SDA(GPIO2/PIN3) </td> <td>BME280のSDIと接続</td>
  </tr>
  <tr>
    <td> SCL(GPIO3/PIN5) </td> <td>BME280のSCKと接続</td>
  </tr>
  <tr>
    <td> Ground(PIN6) </td> <td>接地、グラウンド</td>
  </tr>
</table>

[Raspberry Pi Documentation](https://www.raspberrypi.com/documentation//computers/os.html)

<table>
  <caption>BME280ピン配置</caption>
　<thead>
    <tr>
      <th>BME280</th> <th>説明</th>
    </tr>
  </thead>
  <tr>
    <td> SDO </td> <td>接地、グラウンド</td>
  </tr>
  <tr>
    <td> SCK </td> <td>RasPiのSCL(GPIO3/PIN5)</td>
  </tr>
  <tr>
    <td> SDI </td> <td>RasPiのSDA(GPIO2/PIN3)</td>
  </tr>
　<tr>
    <td> CSB </td> <td>電源</td>
  </tr>
  <tr>
    <td> GND </td> <td>接地、グラウンド</td>
  </tr>
  <tr>
    <td> Vcore </td> <td>接続なし</td>
  </tr>
　<tr>
    <td> Vio </td> <td>電源</td>
  </tr>
</table>

[BME280 Combined humidity and pressure sensor](https://cdn.sparkfun.com/assets/learn_tutorials/4/1/9/BST-BME280_DS001-10.pdf)



## センサが壊れていないか確認
[BME280 Combined humidity and pressure sensor](https://cdn.sparkfun.com/assets/learn_tutorials/4/1/9/BST-BME280_DS001-10.pdf)によると気温25℃で誤差±0.5℃らしい
[BME280活用のサンプルプログラム](https://github.com/SWITCHSCIENCE/samplecodes/blob/master/BME280/Python27/bme280_sample.py)を参考に公開されているMITライセンスのプログラムがあるのでそれを利用させて頂きます。
```
#!/usr/bin/env python

# MIT License
# 
# Copyright (c) 2020 H.Saido <saido.nv@gmail.com>
# Copyright (c) 2018 Switch Science
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from smbus2 import SMBus

class BME280:
    """BME280: Combined humidity and pressure sensor
    https://cdn.sparkfun.com/assets/learn_tutorials/4/1/9/BST-BME280_DS001-10.pdf

    Settings and performance for indoor navigation

    Suggested settings for indoor navigation
    Sensor mode:           normal mode, tstandby = 0.5 ms
    Oversampling settings: pressure ×16, temperature ×2, humidity ×1
    IIR filter settings:    filter coefficient 16

    Performance for suggested settings
    Current consumption: 633 µA
    RMS Noise:           0.2 Pa / 1.7 cm
    Data output rate:    25Hz
    Filter bandwidth:    0.53 Hz
    Response time (75%): 0.9 s

    refer: https://github.com/SWITCHSCIENCE/samplecodes/blob/master/BME280/Python27/bme280_sample.py
    """
    def __init__(self, bus_num=1, i2c_address=0x76):
        self.i2c = SMBus(bus_num)
        self.i2c_address = i2c_address
        self.t_fine = 0.0
        self.pressure = 0.0
        self.temperature = 0.0
        self.humidity = 0.0

        # Fixed settings
        osrs_t = 2 # Temperature oversampling x2
        osrs_p = 5 # Pressure oversampling x16
        osrs_h = 1 # Humidity oversampling x1
        mode = 3 # Normal mode
        t_sb = 0 # tstandby 0.5 ms
        filter = 4 # Filter x16

        self.i2c.write_byte_data(self.i2c_address, 0xF2, osrs_h)
        self.i2c.write_byte_data(self.i2c_address, 0xF4, (osrs_t << 5) | (osrs_p << 2) | mode)
        self.i2c.write_byte_data(self.i2c_address, 0xF5, (t_sb << 5) | (filter << 2))

        self._get_calib_param()


    def _get_calib_param(self):
        calib = []
        # Update by every call
        self.digT = []
        self.digP = []
        self.digH = []

        for i in range (0x88, 0x88+24):
            calib.append(self.i2c.read_byte_data(self.i2c_address, i))
        calib.append(self.i2c.read_byte_data(self.i2c_address, 0xA1))
        for i in range (0xE1, 0xE1+7):
            calib.append(self.i2c.read_byte_data(self.i2c_address, i))

        self.digT.append((calib[1] << 8) | calib[0])
        self.digT.append((calib[3] << 8) | calib[2])
        self.digT.append((calib[5] << 8) | calib[4])
        self.digP.append((calib[7] << 8) | calib[6])
        self.digP.append((calib[9] << 8) | calib[8])
        self.digP.append((calib[11]<< 8) | calib[10])
        self.digP.append((calib[13]<< 8) | calib[12])
        self.digP.append((calib[15]<< 8) | calib[14])
        self.digP.append((calib[17]<< 8) | calib[16])
        self.digP.append((calib[19]<< 8) | calib[18])
        self.digP.append((calib[21]<< 8) | calib[20])
        self.digP.append((calib[23]<< 8) | calib[22])
        self.digH.append( calib[24] )
        self.digH.append((calib[26]<< 8) | calib[25])
        self.digH.append( calib[27] )
        self.digH.append((calib[28]<< 4) | (0x0F & calib[29]))
        self.digH.append((calib[30]<< 4) | ((calib[29] >> 4) & 0x0F))
        self.digH.append( calib[31] )

        for i in range(1,2):
            if self.digT[i] & 0x8000:
                self.digT[i] = (-self.digT[i] ^ 0xFFFF) + 1

        for i in range(1,8):
            if self.digP[i] & 0x8000:
                self.digP[i] = (-self.digP[i] ^ 0xFFFF) + 1

        for i in range(0,6):
            if self.digH[i] & 0x8000:
                self.digH[i] = (-self.digH[i] ^ 0xFFFF) + 1  


    def get(self):
        """Get pressure, temperature, humidity
        pressure: hPa (float)
        temperature: Celsius degree (float)
        humidity: % (float)
        """
        data = [self.i2c.read_byte_data(self.i2c_address, a) for a in range(0xF7, 0xF7+8)]
        pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
        temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
        hum_raw  = (data[6] << 8)  |  data[7]

        self.temperature = self._compensate_T(temp_raw) # update t_fine first
        if pres_raw < 0x80000:
            # Sometimes get strange value
            self.pressure = self._compensate_P(pres_raw)
        self.humidity = self._compensate_H(hum_raw)

        return self.pressure, self.temperature, self.humidity


    def _compensate_P(self, adc_P):
        """Return compensated hPa value
        param: raw pressure ADC data (int)
        return: hPa (float)
        """
        v1 = (self.t_fine / 2.0) - 64000.0
        v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * self.digP[5]
        v2 = v2 + ((v1 * self.digP[4]) * 2.0)
        v2 = (v2 / 4.0) + (self.digP[3] * 65536.0)
        v1 = (((self.digP[2] * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8)  + ((self.digP[1] * v1) / 2.0)) / 262144
        v1 = ((32768 + v1) * self.digP[0]) / 32768
        
        if v1 == 0:
            return 0
        pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125
        if pressure < 0x80000000:
            pressure = (pressure * 2.0) / v1
        else:
            pressure = (pressure / v1) * 2
        v1 = (self.digP[8] * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096
        v2 = ((pressure / 4.0) * self.digP[7]) / 8192.0
        pressure = pressure + ((v1 + v2 + self.digP[6]) / 16.0)  

        # return hPa
        return pressure/100


    def _compensate_T(self, adc_T):
        """Return compensated temperature value
        param: raw temperature ADC data (int)
        return: Celsius degree temperature (float)
        """
        v1 = (adc_T / 16384.0 - self.digT[0] / 1024.0) * self.digT[1]
        v2 = (adc_T / 131072.0 - self.digT[0] / 8192.0) * (adc_T / 131072.0 - self.digT[0] / 8192.0) * self.digT[2]
        self.t_fine = v1 + v2
        temperature = self.t_fine / 5120.0

        return temperature


    def _compensate_H(self, adc_H):
        """Return compensated humidity value
        param: raw humidity ADC data (int)
        return: humidity % (float)
        """
        humidity = self.t_fine - 76800.0
        if humidity != 0:
            humidity = (adc_H - (self.digH[3] * 64.0 + self.digH[4]/16384.0 * humidity)) * (self.digH[1] / 65536.0 * (1.0 + self.digH[5] / 67108864.0 * humidity * (1.0 + self.digH[2] / 67108864.0 * humidity)))
        else:
            return 0

        humidity = humidity * (1.0 - self.digH[0] * humidity / 524288.0)

        if humidity > 100.0:
            humidity = 100.0
        elif humidity < 0.0:
            humidity = 0.0

        return humidity


if __name__ == '__main__':
    bme280 = BME280()
    p, t, h = bme280.get()
    print(f"{p:7.2f} hPa, {t:6.2f} C, {h:5.2f} %")

```
自分が持っているBME280が壊れていないか確認する。
確認方法：BME280から取得した気温と、気温計で測った気温を比較して誤差がどれくらいか調べる。
今回はセンサの選定、検証には重きを置かないため測定は一度で済ませる。



<img width="500" alt="raspi.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3473187/0219e7b2-5b14-ceaf-24b5-e2357c0e794f.png"><img width="200" alt="IMG_0580.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3473187/72b0aa9c-f742-279f-2d7e-7446b1d01acf.jpeg">

両方で測った結果、、センサでは29.15℃、気温計だと28.80~28.90℃なので壊れてないことは確認できた。


## Ras Pi（python）とpostgreの接続を行う
postgre接続参考サイト：[PythonからPostgreSQLにpsycopg2を使って接続する方法](https://www.ashisuto.co.jp/db_blog/article/20160308_postgresql_with_python.html)をもとにpsycopg2をRasPi内に導入する。



### 接続とデータの登録を行う

##### ディレクトリ構造
```
/CmftCon $ tree -L 2
.
├── __init__.py
├── InsertInfo.py
└── UnitTest
    ├── bmeTest.py
    ├── ConnectTest.py
    ├── InsertDataTest.py
    ├── InsertTest.py
    └── __pycache__
```
- `__init__.py`でUnitTestをimportする
- `bmeTest.py`は先程のプログラムを利用する


##### 接続テストプログラム
```ConnectTest.py
import psycopg2
constr = "host='*******' port=5432 dbname=dev user=root password='root'"
conn = psycopg2.connect(constr)

cur = conn.cursor()

cur.execute('select * from weather')

data = cur.fetchall()
print(data)
cur.close()
conn.close()
```

##### 出力
```
[Running] python -u "/home/pi/CmftCon/UnitTest/ConnectTest.py"
[(1, 0, 0, 0, 0, False, True, datetime.datetime(2023, 3, 1, 0, 0))]

[Done] exited with code=0 in 0.284 seconds
```
接続テストが完了し、テストデータを取得することができた。


##### データ登録テストプログラム1
固定値がpostgreに登録できるかどうか
```InsertTest.py
import psycopg2
constr = "host='*******' port=5432 dbname=dev user=root password='root'"
conn = psycopg2.connect(constr)

cur = conn.cursor()

cur.execute("INSERT INTO weather (temp,humid,pressure,comfort,deal_flg,del_flg,date) values ( 30,56,1090,80,'false','false','2023-04-01');")
cur.execute('select * from weather')
conn.commit()
data = cur.fetchall()
print(data)
cur.close()
conn.close()
```
`conn.commit()`がないとINSERTした結果がデータベースに変更が保存されない。（ここに意外と躓いた、、）
##### 出力
```
[Running] python -u "/home/pi/CmftCon/UnitTest/InsertTest.py"
[(1, 0, 0, 0, 0, False, True, datetime.datetime(2023, 3, 1, 0, 0)), (2, 30, 56, 1090, 80, False, False, datetime.datetime(2023, 4, 1, 0, 0))]

[Done] exited with code=0 in 0.274 seconds
```

##### データ登録テストプログラム2
postgreに日付型データが登録できるかどうか
```InsertDateTest.py
import datetime
import psycopg2

def InsertInit():
    dt_now = datetime.datetime.now()
    stringdt = str(dt_now.strftime('%Y-%m-%d %H:%M:%S'))
    constr = "host='*******' port=5432 dbname=dev user=root password='root'"
    connect = psycopg2.connect(constr)
    cursor = connect.cursor()
    
    return stringdt, cursor, connect

def Insert(que):
    str_dt, cur, conn = InsertInit()
    value = (str_dt,)
    cur.execute(que, value)
    conn.commit()
    cur.execute('select * from weather')
    data = cur.fetchall()
    print(data)
    cur.close()
    conn.close()

if __name__ == '__main__':
    que = "INSERT INTO weather (temp,humid,pressure,comfort,deal_flg,del_flg,date) values ( 30,56,1090,80,'false','false',%s);"
    Insert(que)
```

##### 出力
```
[Running] python -u "/home/pi/CmftCon/UnitTest/InsertDataTest.py"
[(1, 0, 0, 0, 0, False, True, datetime.datetime(2023, 3, 1, 0, 0)), (3, 30, 56, 1090, 80, False, False, datetime.datetime(2023, 9, 22, 10, 21))]

[Done] exited with code=0 in 0.322 seconds
```

#### 本番用プログラムの作成
それぞれ必要な機能が上手く動作することがわかったので本番用のプログラムを作成する。
```
import UnitTest.bmeTest
import UnitTest.InsertDataTest
import datetime
import math
import schedule
import time

#インスタンス生成
bme280 = UnitTest.bmeTest.BME280()
insert = UnitTest.InsertDataTest

#気温、湿度、気圧を取得
def getWeather():
    p_raw, t_raw, h_raw = bme280.get()
    p = f"{p_raw:7.2f}"
    t = f"{t_raw:7.2f}"
    h = f"{h_raw:7.2f}"
    print(p,t,h)

    #不快指数　https://keisan.casio.jp/exec/system/1202883065
    comfort_raw = 0.81*float(t)+0.01*float(h)*(0.99*float(t)-14.3)+46.3

    print(comfort_raw)
    c = math.ceil(comfort_raw)
    print(c)

    dt_now = datetime.datetime.now()
    d = dt_now.strftime('%Y-%m-%d %H:%M:%S')
    value = (float(t),float(h),float(p),c,d,)
    
    return value


import psycopg2

#データベース接続処理
def InsertInit():
    constr = "host='*******' port=5432 dbname=dev user=root password='root'"
    connect = psycopg2.connect(constr)
    cursor = connect.cursor()
    
    return cursor, connect

#データ登録処理
def Insert(que):
    cur, conn = InsertInit()
    cur.execute(que, getWeather())
    conn.commit()
    cur.execute('select * from weather')
    data = cur.fetchall()
    print(data)
    cur.close()
    conn.close()

if __name__ == '__main__':
    #実行SQL
    que = "INSERT INTO weather (temp,humid,pressure,comfort,deal_flg,del_flg,date) values (%s,%s,%s,%s,'false','false',%s);"
    #初回実行
    Insert(que)
    schedule.every(30).minutes.do(Insert, que)
    while True:
        schedule.run_pending()
        time.sleep(1)
```

気温、湿度、気圧、日付を30分毎に取得するために`schedule`ライブラリを利用して定期実行を設定する。

##### 出力
```
[Running] python -u "/home/pi/CmftCon/InsertInfo.py"
1002.34   29.77   68.97
80.87803531
81
[(1, 0, 0, 0, 0, False, True, datetime.datetime(2023, 3, 1, 0, 0)), (3, 30, 56, 1090, 80, False, False, datetime.datetime(2023, 9, 22, 10, 21)), (4, 30, 69, 1002, 81, False, False, datetime.datetime(2023, 9, 22, 10, 28, 53))]
```
データが登録されていることが確認できた！


以上で「Spring × Ras Piでセンサから取得したデータを表示する」が完了しました！！
ただ、、ローカルでdocker立て続けるのはちょっと面倒なのでAWSでやりたいですね、、

[第1弾(Spring編)はこちら](https://qiita.com/mrAkito/items/85d2e3d8baa4f7d0a24e)
[第3弾(AWS編)はこちら](https://qiita.com/mrAkito/items/df21d9f77892886e71f7)
[問題解決編(デーモン化)はこちら](https://qiita.com/mrAkito/items/df21d9f77892886e71f7)

#### おまけと今後の展望
- mysql-driverだと自動でトランザクション処理を行ってくれたが今回扱った接続ライブラリだと`commit`しないと反映されないことに最初気が付かず苦戦した。
    - ロールバック処理もできるみたいなのでデータベースとしての強さは上がったっぽい
- Ras Piとサーバサイドの通信方法を考えたい
    - Python（サーバ）とPython(クライアント)でのソケット通信はやったことあるけど、javaとPythonでもやってみたい
- AWSEC2でdocker-compose、dockerを構築した後、最大限に費用を抑えながら楽をする方法が気になる