はじめに
AmazonでCO2モニターを色々調べてみたものの、ロガー機能やデータの可視化を求めると思ったより高くなりそうだったのと、測定頻度の調整や可視化部分のカスタマイズを自由にやりたかったため、今更ながら試しに自作してみた際の備忘録です。
CO2センサーをラズパイに繋げてリアルタイムで測定し、測定データをラズパイ上に保存できるようにしました。またPythonの可視化ライブラリの一つであるPlotly Dashを使ったダッシュボードをラズパイ上に立ち上げ、ローカルネットワーク内であればブラウザから確認できるようにしました。
やりたいこと
- 部屋のCO2濃度を定時測定し、測定データを保存する
- 測定データを可視化したダッシュボードページをラズパイ上に立ち上げ、ブラウザで確認できるようにする
AWSと連携してアラート設定したり、Googleスプレッドシートにデータを吸い上げることも考えたのですが、今回は試しにラズパイ上だけで構成を完結させました。
買ったもの
-
Raspberry Pi Zero WH
- そこそこ安く、デフォルトでGPIOピンヘッダーがはんだ付けされており、かつWiFi接続できるモデルだったので購入。
-
MH-Z19C
- CO2センサー「MH-Z19」シリーズの一つ。比較的情報が多かったのと在庫が残っていたため購入。
-
GeeekPi Raspberry Pi Zero 2 Wケースキット
- 筐体ケースや電源ケーブル、ヒートシンクなどがまとまったキット。
-
ジャンパーケーブル
- センサーとラズパイ本体を接続するためのケーブル。
-
microSDカード
- ラズパイOSのインストール先となるmicroSDカード。
全部あわせて11000円くらい(送料込み)でした。
やったこと
工作
特別な工具も必要なく、10分くらいで完了できました。
- ラズパイ本体にケースキットを取り付け
- ケースをネジとナットで固定
- ヒートシンク(粘着テープ付き)をCPUに取り付け
- ラズパイ本体とCO2センサーを接続(参考)
- ジャンパーケーブル4本使って接続
ラズパイのセットアップ
既にたくさんの方が記事を書かれてますので、参考にしつつセットアップしていきます。
- microSDにラズパイOSをインストール
-
こちらを参考に、 公式のイメージ書き込みツールを使ってインストールします。今回はラズパイ上での作業は基本的にMacからSSH接続して行うため、GUI無しのバージョン(
Raspberry Pi OS Lite (32-bit)
)を選択しました。5分程度でインストール完了。
-
こちらを参考に、 公式のイメージ書き込みツールを使ってインストールします。今回はラズパイ上での作業は基本的にMacからSSH接続して行うため、GUI無しのバージョン(
- microSDをラズパイに差し込み、電源を入れて起動
- 初回起動時は5分程度で起動完了。
- SSHで接続
- OSインストール時、ユーザー名とホスト名がデフォルトのままであれば
ssh pi@raspberrypi.local
で接続できます。
- OSインストール時、ユーザー名とホスト名がデフォルトのままであれば
- シリアル通信の設定
- センサー値をシリアル通信で取得するための設定が必要とのことなので、 こちらを参考に
sudo raspi-config
コマンドから設定します。
- センサー値をシリアル通信で取得するための設定が必要とのことなので、 こちらを参考に
プログラミング
ここからプログラミングしていきます。まずPythonでセンサー値を取得し、一定時間おきに測定値をcsvファイルに書き出すようにします。また、測定値をPlotly Dashで可視化できるようにします。
以下、作業ディレクトリは/home/pi/raspberrypi_sensor
とします。
準備
Python環境はpython 3.9.2
がデフォルトでインストールされていたので、そのまま使います。
pipは入っていなかったのでインストールします。
sudo apt update
sudo apt -y install python3-pip
jqコマンドをあとで使うのでインストールしておきます。
sudo apt -y install jq
センサーから測定値を毎分取得し、csvファイルに出力
CO2センサーから測定値を取得するため、mh_z19というpythonライブラリを利用させていただきました。
sudo pip install mh_z19
インストール後sudo python -m mh_z19
を実行すると{"co2": 400}
のような形式で測定値が返ってくるので、jqコマンドを使って値のみを抜き出すスクリプトを作ります。
#!/bin/sh
result=$(sudo python -m mh_z19 | jq .co2)
if [ -z "$result" ]; then
echo "None"
else
echo "$result"
fi
さらに、測定値をcsvファイルに書き込むスクリプトも用意します。csvファイルはdataディレクトリ以下に作成されます。
#!/bin/sh
mkdir -p data
current_date=$(date +"%Y-%m-%d")
if [ ! -f data/"$current_date".csv ] ; then
touch data/"$current_date".csv
echo "timestamp co2_ppm" >> data/"$current_date".csv
fi
timestamp=$(date +%s)
co2_ppm=$(bash sampling.sh)
echo "$timestamp $co2_ppm" >> data/"$current_date".csv
毎分測定したいので、write_csv.sh
をcronで定期実行します。crontab -e
コマンドを実行し、以下を追記します。
*/1 * * * * cd /home/pi/raspberrypi_sensor; bash write_csv.sh;
ここまででCO2濃度を毎分測定してcsvファイルに蓄積する仕組みができました。csvファイルには以下のような形式で測定時刻と測定値を記録しています。
timestamp co2_ppm
1669307341 581
1669307401 571
1669307461 568
...
Plotly Dashを使って測定値を可視化
測定データが溜まるようになったので、ダッシュボードを作って可視化します。Pythonで利用できる可視化ライブラリはいくつかあるのですが、今回は個人的に触ったことのあったPlotly Dashを使って簡易ダッシュボードを作りました。
まずPlotly Dashをpipでインストールします(そこそこ時間かかります)。
sudo pip install dash
ダッシュボードを作ります。以下は主要部分のコードだけですが、詳細はこちらに公開していますので、よければご参照ください。
# -*- coding: utf-8 -*-
import csv
import glob
import subprocess
from datetime import datetime
import dash
import plotly.graph_objects as go
from dash import dcc, html
from dash.dependencies import Input, Output
def get_date():
return str(datetime.now()).split(' ')[0]
def get_path(d):
return f'data/{d}.csv'
def get_csv_dates():
files = glob.glob('data/*')
files.sort(reverse=True)
return [f.split('/')[1].split('.')[0] for f in files]
def read_csv(path):
with open(path) as f:
reader = csv.reader(f, delimiter=' ')
data = [row for row in reader]
data_t = [list(x) for x in zip(*data)]
return data_t
def sampling():
sample = int(subprocess.check_output(['bash', 'sampling.sh']))
datetime_now = datetime.now().strftime("%Y/%m/%d - %H:%M:%S")
return f'CO2: {sample} ppm', f'{datetime_now}'
def shutdown():
subprocess.Popen(['bash', 'shutdown.sh'])
def get_co2_fig(path):
data = read_csv(path)
layout = go.Layout(plot_bgcolor='WhiteSmoke', paper_bgcolor='WhiteSmoke')
fig = go.Figure(layout=layout)
timestamp = [datetime.fromtimestamp(int(i)) for i in data[0][1:]]
co2_ppm = [int(i) for i in data[1][1:]]
fig.add_trace(go.Scatter(x=timestamp, y=co2_ppm, line=dict(width=2, color='Crimson')))
fig.update_xaxes(showgrid=False)
fig.update_yaxes(showgrid=False)
fig.update_layout(yaxis_title='CO2 (ppm)',
margin=dict(l=200, r=200, t=10, b=10), showlegend=False,
uirevision='true', height=300)
return fig
graph_types = ['CO2']
csv_dates = get_csv_dates()
current_date = get_date()
co2_fig = get_co2_fig(get_path(current_date))
app = dash.Dash(__name__)
app.title = 'Raspberry Pi Sensor Monitor'
app.layout = html.Div(children=[
html.Br(),
html.H3(children='Raspberry Pi Sensor Monitor', style={'fontFamily': 'Arial Black', 'fontSize': 48}),
html.H3(id='container-sample-main', children=' ', style={'fontFamily': 'Arial Black', 'fontSize': 32}),
html.H6(id='container-sample-sub', children=' '),
html.Hr(),
html.Div(children=[
html.Div(children=[
html.Label('Type'),
dcc.Dropdown(graph_types, value=graph_types[0], id='dropdown_graphtype', style={'textAlign': 'left'})
], style={'width': '20%', 'display': 'inline-block', 'marginRight': 5}),
html.Div(children=[
html.Label('Date'),
dcc.Dropdown(csv_dates, value=current_date, id='dropdown_date', style={'textAlign': 'left'})
], style={'width': '30%', 'display': 'inline-block', 'marginLeft': 5})
], style={'width': '50%', 'display': 'inline-block'}),
dcc.Graph(id='co2-graph',
figure=co2_fig,
config={'displayModeBar': False, 'responsive': False}),
html.Hr(),
html.Div(children=[
html.Button('Shutdown', id='shutdown', n_clicks=0),
html.H6(id='shutdown_message', children='', style={'fontSize': 16}),
html.Br()]),
html.Img(src='assets/plotly_logo.webp', alt='image', style={'width': '12%', 'marginBottom': 40}),
dcc.Interval(id='interval', interval=10000, n_intervals=0)
], style={'textAlign': 'center', 'backgroundColor': 'WhiteSmoke', 'color': '#2F3F5C'})
@app.callback([Output('container-sample-main', 'children'),
Output('container-sample-sub', 'children'),
Output('co2-graph', 'figure'),
Output('dropdown_date', 'options'),
Output('dropdown_date', 'value')],
[Input('interval', 'n_intervals'),
Input('dropdown_date', 'value')])
def trigger_by_interval(n, selected_date):
global current_date, csv_dates, co2_fig
date = get_date()
csv_dates = get_csv_dates()
co2_ppm, updated_time = sampling()
if current_date != date:
current_date = date
print(f'new csv created: {get_path(current_date)}')
co2_fig = get_co2_fig(get_path(current_date))
return co2_ppm, updated_time, co2_fig, csv_dates, current_date
if selected_date is None or selected_date == current_date:
print(f'selected_date: {selected_date} -> use {current_date}.csv')
co2_fig = get_co2_fig(get_path(current_date))
return co2_ppm, updated_time, co2_fig, csv_dates, current_date
else:
print(f'selected_date: {selected_date}.csv exists -> use it')
co2_fig = get_co2_fig(get_path(selected_date))
return co2_ppm, updated_time, co2_fig, csv_dates, selected_date
@app.callback(Output('shutdown_message', 'children'), Input('shutdown', 'n_clicks'))
def shutdown_button_clicked(n_clicks):
if n_clicks > 0:
shutdown()
return 'shutdown signal send...'
else:
return ''
if __name__ == '__main__':
app.run_server(debug=False, host="0.0.0.0")
以下を実行するとローカルホストの8050番にダッシュボードが立ち上がります。
python app.py
できたもの
ローカルネットワーク内からraspberrypi.local:8050
にアクセスすると以下のようなダッシュボードが表示されます。2行目のCO2測定値は10秒に1度センサーから取得し、更新しています。グラフはPlotlyを使って表示しており、インタラクティブに操作できます。またドロップダウンから日付を指定して過去のグラフを見られるようにしています(が、反映は激遅…)。
その他
シャットダウンボタンをつける
ラズパイをシャットダウンするために毎回SSHしてコマンドを叩くのは面倒なので、ダッシュボードページの下部にシャットダウンボタンをつけました。以下のスクリプトをapp.pyから叩いています。
#!/bin/sh
echo "shutdown requested"
sleep 3
sudo shutdown -h now
ラズパイ起動時、自動でダッシュボードを立ち上げる
同様に、ラズパイを再起動したときに毎回SSHしてダッシュボードを立ち上げるのは面倒なので、Systemdを使って自動で立ち上がるように設定しました。以下の設定ファイルを作成します。
[Unit]
Description=Run sensor monitor
After=network.target
[Service]
ExecStart=/usr/bin/python /home/pi/raspberrypi_sensor/app.py
WorkingDirectory=/home/pi/raspberrypi_sensor
Restart=always
[Install]
WantedBy=multi-user.target
- ExecStartに実行コマンドをフルパスで記述
- WorkingDirectoryに実行ディレクトリを指定
-
Restart=always
を指定することで、プロセスが落ちたら自動復旧
これを/etc/systemd/system/
以下に配置し、以下を実行することで次回起動以降は自動でダッシュボードが立ち上がるようになります。
sudo systemctl enable sensor_monitor.service
自動起動できるか確認してみます。ラズパイを再起動した後SSHで再接続し、以下を実行することでダッシュボードのプロセスが起動しているかを見ます。
ps -aux | grep "/usr/bin/python /home/pi/raspberrypi_sensor/app.py"
以下が出力されました。この例ではpid 25414
で自動実行できていることがわかります。
pi 21909 0.0 0.4 7436 2024 pts/0 S+ 16:57 0:00 grep --color=auto app.py
root 25414 3.2 6.7 73304 29796 ? Ss 02:04 28:38 /usr/bin/python /home/pi/raspberrypi_sensor/app.py
おわりに
ラズパイでのCO2測定は既にかなり多くの方々が試されているのですが、ラズパイ上にダッシュボードを乗せる例はあまりなかったので、(良し悪しはさておき)こういうのもあるんだな〜程度に読んでもらえたら嬉しいです。そのうちCO2以外も測定できるようにしたい。
参考