M5StickCからBLEで送信された温湿度をラズパイでグラフ化してLINEで表示
Contents
グラフ化してLINEで表示
以前にM5StickCに接続した温湿度気圧センサー(BME280)で測定した情報をBLE(Bluetooth Low Energy)でラズパイに通信する記事を書いた。
当時の構成図は以下の通り。
この時はラズパイのコンソールに受け取った温湿度気圧を表示しただけだったが、今回はデータを溜め込んでグラフ化してLINEに表示するまでのプログラムを作成してみた。
全体構成図
全体構成図は以下の通り。
M5StickCはWi-Fiや電源が確保できない場所に設置する事も可能な一方、RaspberryPiはWi-Fi、電源を確保できる場所に設置するイメージで構成した。
- M5StickCのHY2.0-4P端子に温湿度・気圧測定センサー(BME280)を取り付ける
- センサーで取得した値をBLEで10秒間アドバタイジング(ブロードキャスト)する
- 数十メートル離れた場所でラズパイ4にてBLEで信号をスキャンして受信した情報をローカルのCSVファイルに蓄える
- 蓄えたデータが一定量になったらmatpoltlibでグラフ化してLINE Notifyで表示する
尚、M5StickCのBluetoothのバージョンはBluetooth 4.2なので見通しの良い場所であれば到達距離が約100mである。
今回30m程の距離でテストをしたが問題無く通信が出来た。
必要な機器
- M5StickC
- Raspberry Pi 4 Model B
- 温湿度、気圧センサー(BME280)
- Grove汎用ケーブル
- ジャンパーワイヤーセット
を用意した。
詳細については以前の「M5StickCとラズパイ4とのBLE(Bluetooth Low Energy)通信を試してみた。」の記事の必要な機器の章を参照して欲しい。
M5StickC は下記の M5StickC Plus に置き換わっている。
スイッチサイエンスでは M5StickC(本体のみ)がまだ販売されていた。
使用した工具
- はんだごて
- はんだ
- スタンドルーペ
- 電工ペンチ
を用意した。
詳細については「M5StickCラズパイ4とのBLE(Bluetooth Low Energy)通信を試してみた。」の記事の使用した工具の章を参照して欲しい。
組み立て
- BME280のはんだ付け
- コネクタの作成
を行った。
詳細については「M5StickCラズパイ4とのBLE(Bluetooth Low Energy)通信を試してみた。」の記事の組み立ての章を参照して欲しい。
配線図
M5StickCとBME280のI2C接続の配線図は以下の通り。
購入したGrove汎用ケーブルの黄色と白の色が逆転している(SDAを黄色、SCLを白にしたかった)がまぁ良しとする。
環境構築
M5StickCとRaspberryPi 4に対して事前の環境構築を行う。
M5StickC側
最初にM5StickC側の環境構築。
プログラムの開発環境であるWindows10マシンに、
- M5StickCの開発環境のArduino IDEのインストール
- Arduino IDEに必要なライブラリー(BME280関連)のインストール
を行った。
Windows10マシンへのArduino IDE(統合開発環境)のインストール方法は以前の「Arduino IDEで簡単なプログラム(C++)を作成してM5StickCで実行してみた」の記事を参照して欲しい。
またArduino IDEへのBME280関連のライブラリーのインストールについては「M5StickCラズパイ4とのBLE(Bluetooth Low Energy)通信を試してみた。」の記事の環境構築の章を参照して欲しい。
ラズパイ側
続いてRaspberryPi 4側の事前の環境構築を行う。
LINE Notifyでアクセストークンの発行
LINEにグラフを送信するためにLINE Notifyにログインをしてアクセストークンを発行する。
LINEアプリに設定したメールアドレスとパスワードでログインをする。
右上のメニューから「マイページ」を選択する。
アクセストークンの発行(開発者向け)の「トークンを発行する」をクリックする。
- トークン名:温湿度・気圧・電圧
- 1:1でLINE Notifyから通知を受け取る
を選択して「発行する」ボタンをクリックする。
トークンが発行されるのでコピーをしたら閉じる。
トークンは後ほど、プログラムで使用するので保存しておく。
連携中サービとして表示される。
bluepyのインストール
- ラズパイにBluetoothデバイスを操作するためのモジュール(bluepy)のインストール
を行った。
sudo pip3 install bluepy
詳細については「M5StickCラズパイ4とのBLE(Bluetooth Low Energy)通信を試してみた。」の記事の環境構築(ラズパイ)の章を参照して欲しい。
pandasのインストール
情報を蓄積する為のCSVの操作の為にpandasライブラリーを使用している。
pandasはデータ分析用のライブラリなのでcsv操作のみで使用するのはオーバースペック気味なのだが、扱いやすいのと今後プログラムを改修してより複雑な操作をすることもあるかと思い pandas を選択した。
下のコマンドでインストールを行う。
sudo apt-get install python3-pandas
日本語フォントのインストール
RaspberryPiに日本語フォントのインストールを行った。
ラズパイでの温湿度等のグラフ化にはmatplotlibを使用したのだが標準では日本語が表示できない。
グラフのタイトルや縦横軸の説明に日本語を使用したかったので、
- IPA日本語フォントをインストール
- matplotlibrcの設定ファイル(/etc/matplotlibrc)を編集して日本語IPAフォントを指定
を行った。
sudo apt install fonts-ipaexfont
/etc/matplotlibrc をエディターで編集して font.family と font.sans-serif に IPAexGothic を指定した(下記画面ショット参照)
詳細については「ラズパイに日本語フォントをインストールしてmatplotlibで日本語表示させた件」の記事を参照して欲しい。
プログラム
M5StickC側のプログラム(スケッチ)とラズパイ側のプログラムの両方がある。
M5StickC
今回、M5StickC側のプログラム(c++)は以前に作成したものと違いが無い。
- BME280から温度、湿度、気圧の情報を取得する
- BLE(Bluetooth Low Energy)で10秒間アドバタイジング(ブロードキャスト)する
- その他の時間はDeep Sleepする
の処理を行っている。
詳細については「M5StickCラズパイ4とのBLE(Bluetooth Low Energy)通信を試してみた。」の記事のプログラムの章を参照して欲しい。
ソースコード
ble_pub.ino
#include <M5StickC.h>
#include <BLEDevice.h> // Bluetooth Low Energy
#include <BLEServer.h> // Bluetooth Low Energy
#include <BLEUtils.h> // Bluetooth Low Energy
#include <esp_sleep.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define T_PERIOD 10 // アドバタイジングパケットを送る秒数
#define S_PERIOD 290 // Deep Sleepする秒数
RTC_DATA_ATTR static uint8_t seq; // 送信SEQ
Adafruit_BME280 bme;
uint16_t temp; // 温度
uint16_t humid; // 湿度
uint16_t press; // 気圧
uint16_t vbat; // 電圧
void setAdvData(BLEAdvertising *pAdvertising) { // アドバタイジングパケットを整形する
BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
oAdvertisementData.setFlags(0x06); // BR_EDR_NOT_SUPPORTED | General Discoverable Mode
std::string strServiceData = "";
strServiceData += (char)0x0c; // 長さ(12Byte)
strServiceData += (char)0xff; // AD Type 0xFF: Manufacturer specific data
strServiceData += (char)0xff; // Test manufacture ID low byte
strServiceData += (char)0xff; // Test manufacture ID high byte
strServiceData += (char)seq; // シーケンス番号
strServiceData += (char)(temp & 0xff); // 温度の下位バイト
strServiceData += (char)((temp >> 8) & 0xff); // 温度の上位バイト
strServiceData += (char)(humid & 0xff); // 湿度の下位バイト
strServiceData += (char)((humid >> 8) & 0xff); // 湿度の上位バイト
strServiceData += (char)(press & 0xff); // 気圧の下位バイト
strServiceData += (char)((press >> 8) & 0xff); // 気圧の上位バイト
strServiceData += (char)(vbat & 0xff); // 電池電圧の下位バイト
strServiceData += (char)((vbat >> 8) & 0xff); // 電池電圧の上位バイト
oAdvertisementData.addData(strServiceData);
pAdvertising->setAdvertisementData(oAdvertisementData);
}
void setup() {
M5.begin();
setCpuFrequencyMhz(80); // CPU周波数80以上にしないと無線は使用できない
M5.Axp.begin(false,false,false,false,true); // 省電力の為、DCDC3をオフ
M5.Axp.ScreenBreath(7); // 画面の輝度を下げる
Wire.begin(); // I2Cの初期化
while (!bme.begin(0x76)) { // BME280の初期化
break;
}
temp = (uint16_t)(bme.readTemperature() * 100); // 温度の取得(100倍して小数点以下を整数部へ)
humid = (uint16_t)(bme.readHumidity() * 100); // 湿度の取得(100倍して小数点以下を整数部へ)
press = (uint16_t)(bme.readPressure()/100); // 気圧の取得(pa→hPaに変換」)
vbat = (uint16_t)(M5.Axp.GetVbatData() * 1.1 / 1000 * 100); // バッテリーの電圧
BLEDevice::init("blepub-01"); // デバイスを初期化
BLEServer *pServer = BLEDevice::createServer(); // サーバーを生成
BLEAdvertising *pAdvertising = pServer->getAdvertising(); // アドバタイズオブジェクトを取得
setAdvData(pAdvertising); // アドバタイジングデーターをセット
pAdvertising->start(); // アドバタイズ起動
delay(T_PERIOD * 1000); // T_PERIOD秒アドバタイズする
pAdvertising->stop(); // アドバタイズ停止
seq++; // シーケンス番号を更新
delay(10);
esp_deep_sleep(1000000LL * S_PERIOD); // S_PERIOD秒Deep Sleepする
}
void loop() {
}
ラズパイ4
データを受け取ってLINEに表示するRaspberry Pi 4Bのプログラム(Python)について説明する。
ディレクトリ構造
プログラムは /opt/ 配下に temp-sns というディレクトリを作成して格納した。
ubuntu の説明では /opt は主にサードパーティ製のプログラムや自作のプログラムを格納する場所とあったのでこのディレクトリにしている。
今回は /opt/ 配下に格納したが、特定のユーザからしか起動しないプログラムなので /usr/local/ への格納でも良かったのかも知れない。
├─opt
│ │
│ ├─temp-sns
│ │ │ temp_humi_sns.py
│ │ │ initial.json
│ │ │
│ │ ├──backup
│ │ │ envYYYY-MM-DD-HH-mm-SS.csv
│ │ │ ・
│ │ │ ・
│ │ │
│ │ ├──cert
│ │ │ tempsns_cert.json
│ │ │
│ │ ├──csv
│ │ │ env.csv
│ │ │
│ │ ├──images
│ │ │ pres_volt.png
│ │ │ temp_humi.png
│ │ │
│ │ └──log
│ │ error.log
│ │
temp-sns
temp_humi_sns.py | プログラム本体(ソースコードは後述) | ||||||||||||
initial.json | 初期設定ファイル
|
backup
測定した温湿度、気圧・電圧の情報をLINE送信後にバックアップの意味合いでこのディレクトリに保存する。
ファイル名はenvYYYY-MM-DD-HH-mm-SS.csvの形式で保存する。
cert
認証情報が保存されているディレクトリ。
line_token | 「LINE Notifyでアクセストークンの発行」の章で発行したLINEのアクセストークン |
csv
温湿度、気圧・電圧の情報をLINEに送信する前にローカルディスクに一時的保存するファイルを格納するディレクトリ。
env.csvのファイル名で保存している。
log
各種ログを保存する為のディレクトリ。
Pythonのプログラム中で何らかの例外が発生した時にerror.logのファイル名でエラーメッセージ等を保存している。
プログラムの異常終了時にエラーの原因を究明する為に記録している。
ソースコード
temp_humi_sns.py
# -*- coding: utf-8 -*-
"""
Created on Sun Feb 7 12:55:50 2021
・M5StickCからBLEで受信した温度、湿度、気圧、電圧の情報をcsvに保存する
・一定量(max_line_count)超えたらmatplotlibでグラフ化してLINE NotifyでLINEに送信する
@author: Souichirou Kikuchi
"""
from bluepy.btle import DefaultDelegate, Scanner, BTLEException # BLE関係
import sys
import csv # CSVファイル
import json # JSONファイル
import struct # Packされたバイト列を操作
import os
import shutil # ファイル操作
from datetime import datetime as dt
import matplotlib.pyplot as plt # グラフ作成用
import pandas as pd # CSV用
from concurrent.futures import ThreadPoolExecutor # スレッド処理
import requests # LINEメッセージ
ERROR_LOG = './log/error.log' # エラーログ
INITIAL_FILE = './initial.json' # 初期設定ファイル
LINE_URL = 'https://notify-api.line.me/api/notify' # LINEメッセージ用URL
class ScanDelegate(DefaultDelegate): # BLEのScanクラス
def __init__(self): # コンストラクタ
with open(INITIAL_FILE) as f: # 初期設定ファイルの読み込み
__jsn = json.load(f)
self.env_log_csv = __jsn['env_log_csv'] # 温湿度・気圧・電圧情報保存CSV
self.backup_drive = __jsn['backup_drive'] # バックアップ保存用ドライブ
self.cert_file = __jsn['cert_file'] # 認証情報格納場所
self.temp_humi_img = __jsn['temp_humi_img'] # 温湿度グラフ画像の保存場所
self.pres_volt_img = __jsn['pres_volt_img'] # 気圧、電圧グラフ画像の保存場所
self.max_line_count = __jsn['max_line_count'] # 最大行数(この行数を超えたらグラフを作成してLINEに通知)
f.close()
DefaultDelegate.__init__(self)
self.lastseq = None
self.lasttime = dt.fromtimestamp(0)
def handleDiscovery(self, dev, isNewDev, isNewData):
try:
if isNewDev or isNewData: # 新しいデバイスまたは新しいデータ
for (adtype, desc, value) in dev.getScanData(): # データの数だけ繰り返す
if desc == 'Manufacturer' and value[0:4] == 'ffff': # テスト用companyID
__delta = dt.now() - self.lasttime
# アドバタイズする10秒の間に複数回測定されseqが加算されたものは捨てる(最初に取得された1個のみを使用する)
if value[4:6] != self.lastseq and __delta.total_seconds() > 11:
self.lastseq = value[4:6] # Seqと時刻を保存
self.lasttime = dt.now()
(temp, humid, press, volt) = struct.unpack('<hhhh', bytes.fromhex(value[6:])) # hは2Byte整数(4つ取り出す)
# print('温度= {0} 度、 湿度= {1} %、 気圧 = {2} hPa、 電圧 = {3} V'.format( temp / 100, humid / 100, press, volt/100))
if (os.path.isfile(self.env_log_csv)): # ファイルが存在しているとき
__df = pd.read_csv(self.env_log_csv, header=0, encoding='UTF-8') # 読み込み 0行目がヘッダー(有り)
else: # ファイルが無ければヘッダーを作成
__df = pd.DataFrame(columns=['datetime', 'temp', 'humi', 'press', 'volt'])
__blelog = pd.Series( ['{0:%Y-%m-%d %H:%M:%S.%f}'.format(dt.now()), temp / 100, humid / 100, press, volt/100], index=__df.columns)
__df = __df.append(__blelog, ignore_index=True ) # dataframeを作成して行追加、ignore_indexで新たな行番号を振っている
__df.to_csv(self.env_log_csv, index=False) # Log書き込み indexは書き込まない
if len(__df) >= self.max_line_count: # この行数を超えたらLINEに通知
__executor = ThreadPoolExecutor(max_workers=5) # 同時実行は5つまでスレッド実行
__executor.submit(self.on_theread()) # 別スレッドで実行する
except: # 例外時
ex, ms, tb = sys.exc_info()
self.put_error_log(type(ms)) # エラーログ処理
def on_theread(self): # グラフを作成してLINEに送信は時間がかかるので別スレッドで行う
try:
self.drawing_graph() # グラフを作成&ローカルディスク保存
self.line_message() # グラフをLINE送信
__file_time_stamp = '{0:%Y-%m-%d-%H-%M-%S}'.format(dt.now()) # 日付時刻をセット
__backup_file_path = self.backup_drive + 'env' + __file_time_stamp + '.csv' # ディレクトリ、ファイル名をセット
shutil.move(self.env_log_csv, __backup_file_path) # ファイルをバックアップディレクトリに移動
except: # 例外時
ex, ms, tb = sys.exc_info()
self.put_error_log(type(ms)) # エラーログ処理
def drawing_graph(self): # グラフ表示後、ローカルディスクに保存
try:
dttm = [] # 日付初期化
temp = [] # 温度初期化
humi = [] # 湿度初期化
pres = [] # 気圧初期化
volt = [] # 電圧初期化
fp = open(self.env_log_csv, 'r') # CSVファイルの読み込み
reader = csv.reader(fp)
next(reader) # Headerスキップ
for row in reader: # 行数分だけ繰り返して配列の後ろからセットしてゆく
dttm.append(row[0][0:19]) # 日付時刻を後ろに追加
temp.append(float(row[1])) # 温度を後ろに追加
humi.append(float(row[2])) # 湿度を後ろに追加
pres.append(int(row[3])) # 気圧を後ろに追加
volt.append(float(row[4])) # 電圧を後ろに追加
fp.close()
fig1 = plt.figure() # fig:温湿度のグラフを描画する領域
ax1 = fig1.add_subplot(1, 1, 1) # 1行、1列、1場所に追加
ax2 = ax1.twinx() # X軸を共有
ax1.set_ylim([0, 100]) # 湿度の表示範囲(0~100%)
ax1.set_ylabel('湿度') # Y軸のラベル
ax1.grid(axis='y') # グリッド横表示
ax1.set_xticklabels(dttm, rotation=70, ha='right') # X軸のラベルを傾ける
ax1.bar(dttm, humi, color='b', label='湿度') # Blue
ax2.set_ylim([-45, 85]) # 温度の表示範囲(-45~85度)
ax2.set_ylabel('温度') # Y軸のラベル
ax2.plot(dttm, temp, color='r', marker='o', label='温度') # Red circle marker
h1, l1 = ax1.get_legend_handles_labels() # ラベルを取得
h2, l2 = ax2.get_legend_handles_labels()
ax1.legend(h1+h2, l1+l2, loc='upper left') # 凡例を表示
plt.title('湿度・温度の推移') # グラフタイトル
plt.savefig(self.temp_humi_img, bbox_inches='tight') # 温湿度のファイルを書き出し
fig2 = plt.figure() # fig:気圧と電圧のグラフを描画する領域
ax21 = fig2.add_subplot(1, 1, 1) # 1行、1列、1場所に追加
ax22 = ax21.twinx() # X軸を共有
ax21.set_ylim([0, 10]) # 電圧の表示範囲(0~100%)
ax21.set_ylabel('電圧') # Y軸のラベル
ax21.grid(axis='y') # グリッド横表示
ax21.set_xticklabels(dttm, rotation=70, ha='right') # X軸のラベルを傾ける
ax21.bar(dttm, volt, color='r', label='電圧') # Red
ax22.set_ylim([0, 1200]) # 気圧の表示範囲(-45~85度)
ax22.set_ylabel('気圧') # Y軸のラベル
ax22.plot(dttm, pres, color='b', marker='o', label='気圧') # Red circle marker
h1, l1 = ax21.get_legend_handles_labels() # ラベルを取得
h2, l2 = ax22.get_legend_handles_labels()
ax21.legend(h1+h2, l1+l2, loc='upper left') # 凡例を表示
plt.title('電圧・気圧の推移') # グラフタイトル
plt.savefig(self.pres_volt_img, bbox_inches='tight') # 気圧と電圧のファイルを書き出し
except: # 例外時
ex, ms, tb = sys.exc_info()
self.put_error_log(type(ms)) # エラーログ処理
def line_message(self): # グラフをLINE Notifyから送信
try:
with open(self.cert_file) as f: # 認証情報ファイルの読み込み
__jsn = json.load(f)
line_token = __jsn['line_token'] # LINE Notifyのアクセストークン
f.close()
__headers = {'Authorization' : 'Bearer ' + line_token}
__message = '温度・湿度'
payload = {'message' : __message}
__files = {'imageFile': open(self.temp_humi_img, 'rb')} # 画像ファイル
requests.post(LINE_URL, headers=__headers, params=payload, files=__files)
__message = '気圧・電圧'
payload = {'message' : __message}
__files = {'imageFile': open(self.pres_volt_img, 'rb')} # 画像ファイル
requests.post(LINE_URL, headers=__headers, params=payload, files=__files)
except: # 例外時
ex, ms, tb = sys.exc_info()
self.put_error_log(type(ms)) # エラーログ処理
def put_error_log(self, message): # エラーログファイルを出力する
if (os.path.isfile(ERROR_LOG)): # ファイルが存在しているとき
__df = pd.read_csv(ERROR_LOG, header=0, encoding='UTF-8') # 読み込み 0行目がヘッダー(有り)
else: # ファイルが無ければヘッダーを作成
__df = pd.DataFrame(columns=['ErrorData', 'ErrorMessage'])
__errorlog = pd.Series( ['{0:%Y-%m-%d %H:%M:%S.%f}'.format(dt.now()), message], index=__df.columns)
__df = __df.append(__errorlog, ignore_index=True ) # dataframeを作成して行追加、ignore_indexで新たな行番号を振っている
__df.to_csv(ERROR_LOG, index=False) # Log書き込み indexは書き込まない
if __name__ == '__main__':
scanner = Scanner().withDelegate(ScanDelegate())
while True:
try:
scanner.scan(5.0) # スキャンする。デバイスを見つけた後の処理はScanDelegateに任せる
except BTLEException:
ex, ms, tb = sys.exc_info()
print('BLE exception '+str(type(ms)) + ' at ' + sys._getframe().f_code.co_name)
補足説明
細かい部分はソースコード中のコメントを確認してほしい。
以前の「M5StickCとラズパイ4とのBLE(Bluetooth Low Energy)通信を試してみた。」の記事のプログラムとの相違点を主に記述する。
init
初期設定ファイル(initial.json)ファイルから各種設定情報を読み込む。
handleDiscoveryにてスレッド
./csv/env.csv の行数が max_line_count を超えた時にスレッド処理を呼び出している。
蓄積データのグラフ化とLINE Notifyによる送信は時間がかかる可能性があるのでM5StickCからのデータを取りこぼす事の無いように別スレッドにて処理する。
on_theread
グラフ作成関数とLINE送信関数を呼び出した後でファイルをバックアップする。
drawing_graph
CSVデータを元に matplotlib でグラフを作成する。
グラフは温度・湿度のグラフと気圧・電圧の2種類のグラフを作成してそれぞれpngファイルにして保存する。
また一つのグラフで左右で2軸(温度と湿度)を表示する為に add_subplot で軸を追加している。
line_message
LINEのアクセストークンを使用してLINE Notifyからメッセージを送信する。
プログラムの実行
M5StickCのプログラムは電源ボタンの長押しの電源オンと同時に自動的に開始され、およそ5分間隔で温湿度、気圧、電圧の情報をアドバタイジング(ブロードキャスト)する。
Raspberry Pi 4BのプログラムはXLTerminalから以下のコマンドで実行する。
注意点としてはBluetoothでデバイスにアクセスする為にはroot権限が必要なのでsudoでプログラムを起動する必要がある。
sudo python temp_humi_sns.py
実行結果
M5StickCの電源をオンにするとアドバタイジングが始まり温湿度・気圧センサー(BME280)から取得した値をBLE(Bluetooth Low Enegy)でRaspberryPiに送信する。
Raspberry Piではデータがある一定量蓄積されたところで matplotlib でグラフして後、LINE NotifyでLINEに送信する。
最後に
以上で今回の記事は終了とする。
matplotlibを使えば比較的簡単にグラフ化できる事とLINE Notifyでテキストや画像をLINEに送信できることが確認できた。
また今回はラズパイから直接携帯端末へ情報をグラフ化して表示させたがAWSなどのクラウドにでデータをアップロードしてグラフ化(やデータベース化)する事も可能だ。
クラウドへのデータ送信については以前の「ラズパイゼロで温湿度と気圧、空気の汚れを検出してグラフ化するIoT機器」の記事を参照して欲しい。
この記事が何処かで誰かの役に立つことを願っている。
尚、当記事中の商品へのリンクはAmazonアソシエイトへのリンクが含まれています。Amazonのアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。
最近のコメント