Raspberry piで温度、湿度、気圧を測定してGoogle spreadsheetへ

私のIOT

たくさんRaspberry PIを持ってる。4Bも持ってる。

でもなにも使っていません。

さみしい。

いっぽうで、台風が近づいてくると気圧計(バロメーター)が欲しくなる季節となりました。
以前、アナログのバロメーター持ってたんだけど、本当に台風が近づくとガーッと気圧が下がるのです。
本当に低気圧なんだとおもしろかったのです。

冗談みたいな話だけど、気圧が下がるとお湯の沸点が下がるから、ラーメン屋さんは普段より長く煮る時間をかけるそうな。

完成品で時計に温度と湿度がついているものは多いというか、普通です。
でも気圧計がついているものは高いし、どれも記録までは取ってくれません。

なので、Raspberry PIにセンサーをつけて作って、Googleスプレッドシートにプロットするシステムを作ろうと思い立ちました。
市販品がないならば、作る価値があるし、Googleスプレッドシートから眺めるということは外出先からスマホでも見られます。

用意するもの

  • Raspberry PI 3とか4とか
  • 秋月電子 BME280 温度湿度センサー
  • センサー用ブレッドボード
  • ブレッドボードとRaspberry PIを繋ぐジャンパーケーブル

Raspberry PIはAmazonで買えばいいかと。
Micro SDカード32GB、5V電源(3A以上、Raspberry PI4用と書いてあるものがいい)は最低限必要ですね。
最初のセットアップを始める時にMicro HDMI変換ケーブル、ディスプレイ、USBキーボードがあったほうが便利ではあります。
ただし、最終的にはVNCサーバーとVNCクライアントをお手持ちのパソコンに導入すると、ディスプレイもキーボードも接続しないで単体で立ち上げられ、あとからMacやWindows PCから操作できるので便利です。(VNCの設定はここ)

BME280センサーは定番らしく、開発した方のブログがネットに結構あるのでラクです。(秋月電子の該当モジュールはここ
気温、湿度、気圧、すべて小さなモジュールですみ、小数点以下二桁でてますから、かなりの精度かもしれません。

ただはんだ付けがいります。
さくっと6本ピンをつけるのはいいのです。
面倒なのはI2Cの指定。SPI接続だとなにもジャンパーをショートしなくていいのですが、どの事例もみーんなI2Cを使いJ3をショートしています。これがすごく小さいので虫眼鏡でショートできたか確認してくださいね。

なお、I2Cを使う場合、raspi-configでInterface-Options -> I2C -> Enable YESを指定し、きちんとFinishで終わってからリブートします。

ジャンパーケーブルは片方がRaspberry PIのIOに差し込めるソケット形式で、反対側がブレッドボードに差し込めるようになっている、オスーメスタイプが必要です。

接続

GPIOのレイアウトを復習します。(クリックで拡大)

私はケース入りなので、次のように配線しました。
ファンをもともと1番(3.3V)と6番(GND)に接続していましたが、BME280の最大電源電圧が3.6Vなので1番はBME280に譲り、2番(5V)にファンを繋ぎました。ちょっとうるさい。


あとは、BME280の使い方としてあちこちにあるとおり、
BME280(1) — GPIO(1) 3.3V
BME280(2) — GPIO(9) GND
BME280(3) — 接続なし
BME280(4) — GPIO(3) SDA
BME280(5) — GPIO(9) GND
BME280(6) — GPIO(5) SCL

としてあります。
BME280の2番と5番はブレッドボード上で接続してRaspberry PIにはGNDは一本ですませています。

Raspberry PIの設定

いたるところに載っています。(例えばここ
raspi-configコマンドかGUIからI2CをEnableにします。

sudo apt install i2c-tools
sudo apt python-smbus

で、i2Cのアドレスを確認するツールとPythonでI2Cを操作するものらしいです。
インストール後、コマンドラインで

sudo i2cdetect -y 1

と入力すると、マップが現れ76と表示されます。
これは0x76にI2Cデバイス(BME280)が接続されていることを示します。

BME280の操作

さてBME280の操作ですが、みーんなスイッチサイエンスのライブラリーを使うことに日本では決まっているようです。
ソースを見るとかなり面倒なので、ここでも借用します。

SWITCHSCIENCE/BME280

で、ですね、ライブラリーとして使うために修正(赤字)をいくつか加えましたが、ブログとして表示すると見えないようだ。すまん。

#coding: utf-8
from smbus 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)
    today = datetime.datetime.now()
    timestamp = today.strftime('%Y/%m/%d %H:%M')
    return [timestamp, t, h, p]

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) 

    # print "pressure : %7.2f hPa" % (pressure/100)
    return "%7.2f" % (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
    # print "temp : %-6.2f ℃" % (temperature) 
    return "%.2f" % (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
    # print "hum : %6.2f %" % (var_h)
    return "%.2f" % (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)

setup()
    get_calib_param()


if __name__ == '__main__':
    try:
        readData()
    except KeyboardInterrupt:
        pass

お気づきのように、オリジナルのprintをreturnで値を返すこと、readDataで必要なデータをリストで返すのが主な変更点です。(リストは年月日時分, 温度, 湿度, 気圧の順です)
ファイル名をどこかの例に習って”bme280_custom.py”としました。

呼び出しテストをするプログラムは以下のとおり。

import bme280_custom
import datetime
import os

filename = '/home/pi/Documents/bme280.csv'

list = bme280_custom.readData()
csv = ",".join(list)
f = open(filename,'a')
f.write(csv+"\n")
print(csv)
f.close()

これはプログラム例があったおかげですんなりできました。
次にGoogleスプレッドシートへの書き込みを試します。

Googleスプレッドシート

Googleスプレッドシートへの書き込みを試した例はたくさんありますが、注意点もあります。
注意点のひとつめはoauth2を使うことが推奨されていないという記事がありますが、oauth2以外で認証を得る方法は見つかりませんでした。
oauth2を使います。
2つ目は後ほどgspreadライブラリーでカラムを消すのですが、delete_rowを使うと”delete_rowsを使え”と警告が出ます。しかしdelete_rowsは機能しません。
2021年9月30日現在の注意事項です。

GoogleスプレッドシートをRaspberry Piのpythonから操作する手順は、例えば次のようなリンクにあります。(ここ
よくまとまっている記事ですが、ディレクトリー構造を複雑にする必要がないので設定だけ見ていただいたほうがいいかもしれません。

私が書き込みテストに使ったプログラムは以下のとおり


import time
import gspread
import json
import datetime

from oauth2client.service_account import ServiceAccountCredentials
scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']

credentials = ServiceAccountCredentials.from_json_keyfile_name('/home/pi/xxxx/秘密鍵.json', scope)
gc = gspread.authorize(credentials)
SPREADSHEET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
worksheet = gc.open_by_key(SPREADSHEET_KEY).worksheet('シート名')

today = datetime.datetime.now()
record = [ today.strftime('%Y/%m/%d %H:%M'), 100, 20, 500]
worksheet.append_row(record)
#worksheet.delete_row(2)

record = worksheet.get_all_records()
print(len(record))

ユーザーが変更しなくてはいけないところは、青色部分です。
秘密鍵の入ったJSONファイルの場所(credentials)、SPREADSHEET_KEY、スプレッドシート名は最低限、ユーザーが決めたスプレッドシートの設定にあわせます。

プログラムの最後の2行は全体で何行データがあるかの調査です。

BME280操作とGoogleスプレッドシートの機能を統合する

最終的なPythonプログラムは以下のとおり。
bme280_custom.pyと秘密鍵JSONは同じところに置いてます。
シート名はlogとしました。


#coding: utf-8

import bme280_custom
import gspread
import json
import datetime
import os

NUM_MONTHLY = 4 * 31  # 4 times/day * 1 month

# Get data from bme280

record = bme280_custom.readData()

# Google Spreadsheet access
from oauth2client.service_account import ServiceAccountCredentials
 
scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
 
credentials = ServiceAccountCredentials.from_json_keyfile_name('/home/pi/Downloads/client_secret.json', scope)
 
gc = gspread.authorize(credentials)
 
SPREADSHEET_KEY = 'スプレッドシートのキー'
 
ws = gc.open_by_key(SPREADSHEET_KEY).worksheet('log')

ws.append_row(record)

num_rows = len(ws.get_all_records())

if (num_rows > NUM_MONTHLY) :
	ws.delete_row(2)

プログラム名は”run_bme280.py”としました。

NUM_MONTHLYは簡単にいえばデータ行の上限です。
私の想定では上のプログラムをRaspberry PIのCronで6時間おきに起動するつもりです。
一日4回です。それを一ヶ月31回続けると、それ以上のデータは欲しくないので古いレコードから消すようにしています。
それがプログラムの最後の処理です。

次のように書き込まれます。(ヘッダーラベルは自分であらかじめ書いておきます)

CRONの設定

ここまで来ると道具はそろったのであとは設定だけです。
まずchkconfigを入手します。

sudo apt install chkconfig

cronが動いているか調べます。cronは一定時間、時間間隔でプログラムを起動してくれる管理ツールです。

chkconfig cron

たぶん動いていないはずですので、

sudo systemctrl eneble cron

で起動します。
これでブートのたびにcronを上げ直す必要はないです。chkconfigコマンドでcron onになっているかを確認しておきましょう。
次にcrontabというスケジュール表を作ります。

crontab -e

とコマンドをたたくと編集するエディターを聞いてきます。私のオススメはnanoです。
私は最高気温を測るため、14時を中心に一日4回ということで2時, 8時, 14時, 20時に起動することにしました。

00 2,8,14,20 * * * python /home/pi/xxx/run_bme280.py

としました。
cronの詳細はこれまたあちらこちらで見かけますので調べましょう。
最初はcronで起動してうまくいかないかもしれないのでログを取ります。cronログのとり方はこれまたあちこちで説明がされているので、適当に参照してください。たとえばここ

運用

最初のcronはうまくいきました。
ファンの音が気にならない場所において、しばらく24時間運用してみます。
快調に動いています。
台風が去ったので気圧がぽんと上がりました。
データが一定数になったら、グラフをつけようと思います。

いやぁ、久々に実用的なものを作りました。
こういうモノを作るのはとても楽しいですね。
夜な夜な調べてやってみての積み重ねだったんですが、楽しかった。

固定化

まぁ、いつまでもワイヤーとブレッドボード結線のままなのも、なんなので、こんな基盤(サンハヤト UB-RPI02)買いました。
もうひとつ、BME280モジュール買ったら組み立てます。