【Raspberry Pi】温湿度センサーで取得した値をcsvで出力する方法(BME280)

【ラズパイ】温湿度センサーで取得した値をcsv出力する方法

こんにちは! けい(Twitter)です。

今回は、温湿度センサー(BME280)を使って値を取得し、csvに出力して時系列データとして出力する方法についてまとめていきたいと思います。

必要なもの

ラズパイ本体

私は今回ラズパイ4でプログラムを実行しましたが、長期間の運用をする場合は消費電力が小さいラズパイzeroが最適です。

温湿度センサー(BME280)

BME280は多くのメーカーがモジュールを販売しています。

その中でも、スイッチサイエンスがGitHubでBME280のコードを公開していることもあって、信頼性が高いのでスイッチサイエンスが販売している物を購入しました。

BME280搭載 温湿度・気圧センサモジュール
スイッチサイエンス(Switch Science)

下準備

ラズパイのI2Cをオンにする

ラズパイ含め、様々なマイコンにはI2C通信という通信規格が存在します。
I2C通信は2本の通信線で、複数のデバイスを繋げることができます。

それでは、ラズパイでの設定に移ります。

ラズパイの「設定」->「Raspberry Piの設定」を開いて、インターフェースをクリックします。

そうすると、下のような画面が出てきます。

I2Cを有効にして、再起動するとI2C通信が使えるようになります。

必要なパッケージのインストール

I2C通信をするために、「smbus2」をインストールします。
smbusとは、I2C通信をpythonで行うためのパッケージです。

ラズパイでターミナルを開きます。
次のコードでsmbus2をインストールします。

$ sudo apt update
$ sudo pip3 install smbus2

インストールが完了したら次のような文が表示されます。

Installing collected packages: smbus2
Successfully installed smbus2-0.4.0

これでsmbus2のインストールは完了です。

回路

Raspberry PiとBME280はI2Cで通信を行います。

通常のI2C通信は信号線2本と、電源とGNDの合計4つのピンで通信が可能なのですが、このモジュールはSPI通信も可能なためピンが多くなっています。

BME280のピンラズパイのピン
SDIGPIO2 (SDA)
SCKGPIO3 (SCL)
Vio、CSB3.3V
GND、SDOGND

接続確認

回路の接続が完了したら、BME280がラズパイに認識されているかを次のコマンドで確認します。
ラズパイでターミナルを開いて実行します。

$ sudo i2cdetect -y 1

今回使う回路での結果は、以下のように0x76というアドレスで認識されていると思います。

BME280のSDOを3.3Vに繋ぎ変えると、違うアドレスで認識されます。
0x77という結果になりました。

一応この2つのアドレスを選ぶことができます。
これは複数のI2Cセンサーなどを用いる場合に、アドレスが重複することがあります。それを避けるためだと思われます。

今回はアドレスを0x76で変えずにやっていきます。

サンプルコード

GitHubからダウンロードする

スイッチサイエンスさんが、bme280をpythonで使うためのコードをGitHubで公開してくれているため、そちらを使用させて頂きます。詳細

ラズパイのターミナルを開いて、スイッチサイエンスさんが公開しているコードをwgetコマンドで取得します。
cdコマンドでサンプルコードを保存したいディレクトリに移動してからwgetの下のコマンドを入力します。

$ cd 任意のディレクトリ
$ wget https://raw.githubusercontent.com/SWITCHSCIENCE/BME280/master/Python27/bme280_sample.py

これで「bme280_sample.py」というファイルがダウンロードできたと思います。

もし上のコマンドでダウンロードできない場合は、こちらのリンクよりpythonコードをコピペして使ってください。

サンプルコードの動作確認

サンプルコードの修正(python3用に変換)

サンプルコードはpython2用に書かれたものなので、pyton3で使うためには少しだけプログラムをいじる必要があります。

具体的には、python2ではprint文を以下のように書くのに対して、

print "Hello world"

python3ではprint文を次のように書きます。

print("Hello world")

つまり、括弧()を追加するだけです。

bme280_sample.py95、103、117行目にprint文があるかと思います。

その部分のprint文に括弧をつけて下さい。これで完了です。

実行

ターミナルで、次のようにコマンドを入力します。

$ python3 bme280_sample.py

このような結果が出力されます。

上から順に気温、気圧、湿度が出力されていることが分かります。

csvファイルで出力する

サンプルコードを少し変更してcsvファイルに出力するためのプログラムを作ります。

#coding: utf-8

from smbus2 import SMBus
import time
import datetime

bus_number  = 1
i2c_address = 0x76

bus = SMBus(bus_number)

digT = []
digP = []
digH = []

t_fine = 0.0


def writeReg(reg_address, data):
	bus.write_byte_data(i2c_address,reg_address,data)

def get_calib_param():
	calib = []
	
	for i in range (0x88,0x88+24):
		calib.append(bus.read_byte_data(i2c_address,i))
	calib.append(bus.read_byte_data(i2c_address,0xA1))
	for i in range (0xE1,0xE1+7):
		calib.append(bus.read_byte_data(i2c_address,i))

	digT.append((calib[1] << 8) | calib[0])
	digT.append((calib[3] << 8) | calib[2])
	digT.append((calib[5] << 8) | calib[4])
	digP.append((calib[7] << 8) | calib[6])
	digP.append((calib[9] << 8) | calib[8])
	digP.append((calib[11]<< 8) | calib[10])
	digP.append((calib[13]<< 8) | calib[12])
	digP.append((calib[15]<< 8) | calib[14])
	digP.append((calib[17]<< 8) | calib[16])
	digP.append((calib[19]<< 8) | calib[18])
	digP.append((calib[21]<< 8) | calib[20])
	digP.append((calib[23]<< 8) | calib[22])
	digH.append( calib[24] )
	digH.append((calib[26]<< 8) | calib[25])
	digH.append( calib[27] )
	digH.append((calib[28]<< 4) | (0x0F & calib[29]))
	digH.append((calib[30]<< 4) | ((calib[29] >> 4) & 0x0F))
	digH.append( calib[31] )
	
	for i in range(1,2):
		if digT[i] & 0x8000:
			digT[i] = (-digT[i] ^ 0xFFFF) + 1

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

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

def readData():
	data = []
	for i in range (0xF7, 0xF7+8):
		data.append(bus.read_byte_data(i2c_address,i))
	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]
	
# 	compensate_T(temp_raw)
# 	compensate_P(pres_raw)
# 	compensate_H(hum_raw)
	
	t = compensate_T(temp_raw)
	p = compensate_P(pres_raw)
	h = compensate_H(hum_raw)
	return [t,p,h]

def compensate_P(adc_P):
	global  t_fine
	pressure = 0.0
	
	v1 = (t_fine / 2.0) - 64000.0
	v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * digP[5]
	v2 = v2 + ((v1 * digP[4]) * 2.0)
	v2 = (v2 / 4.0) + (digP[3] * 65536.0)
	v1 = (((digP[2] * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8)  + ((digP[1] * v1) / 2.0)) / 262144
	v1 = ((32768 + v1) * 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 = (digP[8] * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096
	v2 = ((pressure / 4.0) * digP[7]) / 8192.0
	pressure = pressure + ((v1 + v2 + digP[6]) / 16.0)
	return pressure/100

def compensate_T(adc_T):
	global t_fine
	v1 = (adc_T / 16384.0 - digT[0] / 1024.0) * digT[1]
	v2 = (adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0) * digT[2]
	t_fine = v1 + v2
	temperature = t_fine / 5120.0
	return temperature

def compensate_H(adc_H):
	global t_fine
	var_h = t_fine - 76800.0
	if var_h != 0:
		var_h = (adc_H - (digH[3] * 64.0 + digH[4]/16384.0 * var_h)) * (digH[1] / 65536.0 * (1.0 + digH[5] / 67108864.0 * var_h * (1.0 + digH[2] / 67108864.0 * var_h)))
	else:
		return 0
	var_h = var_h * (1.0 - digH[0] * var_h / 524288.0)
	if var_h > 100.0:
		var_h = 100.0
	elif var_h < 0.0:
		var_h = 0.0
	return var_h


def setup():
	osrs_t = 1			#Temperature oversampling x 1
	osrs_p = 1			#Pressure oversampling x 1
	osrs_h = 1			#Humidity oversampling x 1
	mode   = 3			#Normal mode
	t_sb   = 5			#Tstandby 1000ms
	filter = 0			#Filter off
	spi3w_en = 0			#3-wire SPI Disable

	ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode
	config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en
	ctrl_hum_reg  = osrs_h

	writeReg(0xF2,ctrl_hum_reg)
	writeReg(0xF4,ctrl_meas_reg)
	writeReg(0xF5,config_reg)
	
def write_csv(t,p,h):
    dt_now = datetime.datetime.now()
    with open('temp.csv', 'a') as f:
        f.write(dt_now.strftime('%Y/%m/%d %H:%M:%S') + "," + str(t) + "," + str(p) + "," + str(h) + "\n")
        


setup()
get_calib_param()


if __name__ == '__main__':
    try:
        while True:
            [t,p,h] = readData()
            print(str(t) + ":" + str(p) + ":" + str(h) + "\n")
            write_csv(t,p,h)
            time.sleep(60)
    except KeyboardInterrupt:
        pass

変えた所

値を返すようにする

compensate_P、compensate_T、compensate_Hの3つの関数をまず変更します。

これらの関数内で、print文で温度・湿度・気圧を表示するようになっていますが、csvファイルに出力するためにはこれらの値が必要です。
そのため、returnで値を返すように変えます。

csvを出力する関数を作る

新たにwrite_csv(t,p,h)という関数を作ります。ここに温度・湿度・気圧の返り値を入力とします。

また、エクセルでグラフ化する際に、時間のデータも欲しいのでdatetimeモジュールをインポートして、現在の時刻を取得します。

main

mainでは、60秒おきにcsvに出力するように設定します。

出力結果

エクセルで出力結果を見てみます。

今回の時刻データは、Excelで自動的に時刻と認識してくれます。

約12時間分のデータを今回は取得しました。
夜から朝方にかけて部屋の気温が下がっているのが分かります。ですが部屋の気温は2℃しか変わっていないため、ある程度一定に保たれていることも分かります。

まとめ

今回は温度センサーを使って、ラズパイとI2C通信を行い、結果をcsv形式で出力するプログラムを作りました。

まさにラズパイでIOTをしてみました。さらにデータをgoogle driveとか、NASに送信するようにすれば他のマシンからのアクセスがしやすいので、今度はそちらに挑戦していきたいと思います。

では!