本記事は次の記事の続編です。
今回は、NDIR式の二酸化炭素センサーMH-Z19Cの入手と、次の作業に関する記録です。
Raspberry Piに接続できるCO2センサーとしては、NDIR (非分散型赤外線吸収法) 式の「MH-Z19」という型番のものがメジャーであるらしい。そのシリーズである、末尾に「C」が付いたMH-Z19Cと、ジャンパーワイヤーを購入した。
分類 | 品名 | 購入価格(円) | 備考 |
---|---|---|---|
CO2センサー | Winsen MH-Z19C | 3,100 | メルカリ |
ジャンパーワイヤー | ELEGOO JP-EL-CP-004 | 990 | Amazon.co.jp |
ELEGOO 120pcs多色デュポンワイヤー、arduino用ワイヤ—ゲ—ジ28AWG オス-メス オス-オス メス –メス ブレッドボードジャンパーワイヤー
ELEGOO
最初に、届いたCO2センサーのハードウェア的な動作確認をせねばならない。最終的にはUbuntuサーバをやっているラズパイと接続したいわけだが稼働中であり、まずは、普段暇しているRaspberry Pi 400と接続する。Raspberry Pi 400のOSは標準的な「Raspberry Pi OS」にしてあるおかげで、CO2センサーとの接続に関してウェブ上に多くある先人の知見を参照でき、戸惑う部分がない。
次の2つのページを参照して、Rapsberry Pi 400にて、センサーが示すCO2濃度 (単位はppm) の値を取得できた。
部屋の窓を開けてたっぷり外気を取り入れた状態を計測すると、それらしく約400ppmを示す。また、センサーに息を吹きかけるとちゃんと濃度が上がる、という理科の実験をして、うちの子どもの気を引くことにも成功した。
$ sudo python3 -m mh_z19
{"co2": 948}
自宅サーバのRaspberry Pi 4には、2020-11-19の記事にて紹介した状態で、すでに温湿度・気圧センサーのBME280を接続している。今回は、このBME280用の配線が6本あるところに、CO2センサーMH-Z19C用の配線を4本加えることになる。
なお、各センサーとの接続に用いるラズパイ側のGPIOピンは、電源系を別にすると、前者は「SDA」と「SCL」、後者は「TXD」と「RXD」であり、互いに独立している。つまりラズパイ1台に対し、物理的にはこれらセンサー2つを無加工で同時接続可能ということだ。各センサーの「GND (Ground)」として指定のピンがバッティングするところは、どのGNDピンでも良いだろうと判断して配線した。
自宅サーバRaspberry Pi 4のOSであるUbuntu (20.04.1 LTS (Focal)) で、CO2センサーMH-Z19Cを使用する設定は、Raspberry Pi OSの場合とは勝手が異なるらしい。シリアル通信周りが異なるため、と推測している。
今回はいろいろと情報検索したり模索した結果、次の参考ページの「Answer」欄にある情報を元に、
次のような手順で、Ubuntu (Raspberry Pi 4) にてCO2センサーMH-Z19Cを使用できるようにした。一部パスしたところもある。
enable_uart=1
to /boot/config.txt
/boot/firmware/config.txt
にすでにenable_uart=1
が書いてあるためconsole=serial0,115200
from /boot/firmware/cmdline.txt
on Ubuntu /boot/cmdline.txt
on Raspberry Pi OSsudo systemctl stop serial-getty@ttyS0.service && sudo systemctl disable serial-getty@ttyS0.service
pyserial
installed if you’re using the python serial library, not python-serial
from apt
.pyserial
が入っていたのだろう/etc/udev/rules.d/98-mhz19.rules
KERNEL=="ttyS0", SYMLINK+="serial0" GROUP="tty" MODE="0660"
KERNEL=="ttyAMA0", SYMLINK+="serial1" GROUP="tty" MODE="0660"
sudo udevadm control --reload-rules && sudo udevadm trigger
sudo chgrp -h tty /dev/serial0
sudo chgrp -h tty /dev/serial1
sudo adduser $USER tty
sudo adduser $USER dialout
sudo chmod g+r /dev/ttyS0
sudo chmod g+r /dev/ttyAMA0
これらの手順1~10を経ると、Raspberry Pi OSの場合と同様に、Ubuntuでも次のコマンドでCO2濃度の値を取得できた。
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.1 LTS"
$ sudo python3 -m mh_z19
{"co2": 617}
MH-Z19へのアクセスを、sudoせずに、つまりroot権限を使わずに行う方法については、モジュールmh_z19
の作者による次の記事がある。
この記事にしたがって、実際に/dev/serial0
の権限設定を行ってから、--serial_console_untouched
オプションを付けてモジュールmh_z19を一般ユーザ権限で実行すると、MH-Z19の値を読めるようだ。
$ python3 -m mh_z19 --serial_console_untouched
{"co2": 1223}
しかし私の環境の場合、センサーへのアクセスを繰り返して値を読んだ時に、たまに何らかのエラーが出ることがあった (詳細は失念した)。このため今回は動作の安定性を重視し、root権限を使わないアクセスは諦めることにした。
CO2濃度を常時監視できるようになったので、次は、リビングのCO2濃度が高まったら、住人へ、換気を促す仕組みを作る。促す際には部屋のスマートスピーカーをしゃべらせることにする。
以降の内容は、私の家の環境に完全に依存したものになってしまうことご了承ください。一部分でも参考になれば幸いです。
今回用意する仕組みに必要な、前提条件はおよそ次の通り。
google-home-notifier
を稼働させている (参考: Google Home/Nestを喋らせるgoogle-home-notifierの導入マニュアル [2020年12月版] )root権限のcronを使ってサーバ上で24時間実行する前提の、bashスクリプトを作成した (ソースコードは最後に掲載)。次のような動作をする。
mh-z19_thingspeak.sh
を動かすため、root権限のcrontabを次のように設定している。
FILE_CO2=/tmp/co2_value.dat
*/1 * * * * timeout 10s 【設置ディレクトリ】mh-z19_thingspeak.sh >/dev/null; sleep 10; timeout 10s 【設置ディレクトリ】mh-z19_thingspeak.sh 【ThingSpeak_WRITE_API_KEY】 >${FILE_CO2}.tmp 2>&1; chmod o+r ${FILE_CO2}.tmp; mv ${FILE_CO2}.tmp ${FILE_CO2}
一般ユーザ権限のcronを使ってサーバ上で毎日8時台~21時台に実行する前提の、bashスクリプトも作成した (ソースコードは最後に掲載)。次のような動作をする。
co2_alert.sh
を動かすため、一般ユーザ権限のcrontabを次のように設定している。
FILE_CO2=/tmp/co2_value.dat
*/1 8-21 * * * sleep 40; timeout 10s 【設置ディレクトリ】co2_alert.sh ${FILE_CO2} >/dev/null 2>&1
リビングのCO2濃度を測定して可視化したことで、気づいたこと。
Googleスマートスピーカーに喋らせる部分は、co2_alert.shを見ると分かるように、音声ファイルをffmpeg
で結合することで、それらしいジングル (キャッチーな音) が各音声ファイルの冒頭に入るよう工夫した。(なお、google-home-notifier
の通常のテキスト→音声化を用いつつ、切れ目なくジングルを挿入する方法は思いつかなかった)
また、流暢な発声をするVoiceText Web APIには感情のパラメータもあるので、特に換気完了のお知らせはemotion=happiness, emotion_level=3
と陽気な感じに設定し、アニメのキャラクターボイスのように溌剌としゃべってもらっている。
アラートの通知のあり方として、通知音にどのような工夫ができるかを試すのは楽しかったし、今回の通知方法は家族の受けも良いようで、スマートスピーカー連携を作った甲斐がある。昨今のコロナ渦で注目されている換気の重要性を、私も含め家族へ身近なところから意識させることにつながるかな。
#!/bin/bash
# mh-z19_thingspeak.sh
THINGSPEAK_APIKEY=$1
THINGSPEAK_FIELD='field4' # 各自のThingSpeakに合わせて要調整
THINGSPEAK_URL='https://api.thingspeak.com/update'
if [ `whoami` == "root" ]; then
VALUE_CO2=`python3 -m mh_z19 | jq -r '.co2'`
else
VALUE_CO2=`python3 -m mh_z19 --serial_console_untouched | jq -r '.co2'`
fi
echo ${VALUE_CO2}
if [ -n "${THINGSPEAK_APIKEY}" ]; then
curl -s ${THINGSPEAK_URL} -X POST -d "${THINGSPEAK_FIELD}=${VALUE_CO2}" -H "X-THINGSPEAKAPIKEY: ${THINGSPEAK_APIKEY}" >/dev/null
fi
#!/bin/bash
# co2_alert.sh
# usage: $0 <filename>
if [ $# != 1 ]; then
exit 1
fi
CO2_NOW=$(cat "$1" | awk '{print $1}')
if [ ${CO2_NOW} -lt 1 ]; then
exit 2
fi
CO2_ALERT=1500
CO2_OK=1000
SEC_ALERT=1800
TEXT_CO2_OK="${CO2_NOW}ppmまで下がりました。換気完了です!"
TEXT_CO2_ALERT="二酸化炭素濃度が現在${CO2_NOW}ppm。少し換気をしましょう!"
DIR_BASE='【設置ディレクトリ】'
DIR_SOUND="${DIR_BASE}/cache/"
mkdir -p ${DIR_SOUND}
FILE_SOUND_IN_OK="${DIR_BASE}/News-Accent01-1.mp3" # https://otologic.jp/free/se/news-accent01.html
FILE_SOUND_IN_ALERT="${DIR_BASE}/News-Alert03-1.mp3" # https://otologic.jp/free/se/news-alert01.html
TYPE_SOUND='wav'
FILE_SOUND_OUT="/tmp/co2.${TYPE_SOUND}" # ウェブサーバからアクセスできるように別途工夫 (例: symlink)
FILE_STATUS='/tmp/co2_status.dat'
TMP_FILE=$(mktemp --suffix=.wav -u)
# 鉄板のtmpfile処理 https://fumiyas.github.io/2013/12/06/tempfile.sh-advent-calendar.html
func_atexit() {
#[[ -n ${DIR_TMP-} ]] && rm -rf "${DIR_TMP}"
[[ -n ${TMP_FILE-} ]] && rm -f "${TMP_FILE}"
}
trap func_atexit EXIT
trap 'rc=$?; trap - EXIT; func_atexit; exit $?' INT PIPE TERM
func_savevoice() {
TMP_FILENAME=$1
TMP_TEXT=$2
TMP_EMOTION=$3
TMP_EMOTIONLEVEL=$4
VOICETEXT_APIKEY='【VoiceText API KEY】'
if [ -z "${TMP_EMOTION}" ]; then
curl "https://api.voicetext.jp/v1/tts" \
-o "${TMP_FILENAME}" \
-u "${VOICETEXT_APIKEY}:" \
-d "text=${TMP_TEXT}" \
-d "speaker=hikari" >/dev/null
else
curl "https://api.voicetext.jp/v1/tts" \
-o "${TMP_FILENAME}" \
-u "${VOICETEXT_APIKEY}:" \
-d "text=${TMP_TEXT}" \
-d "speaker=hikari" \
-d "emotion=${TMP_EMOTION}" \
-d "emotion_level=${TMP_EMOTIONLEVEL}" >/dev/null
fi
}
func_voice() {
CO2_STATUS=$1
TEXT_CO2=$2
if [ "${CO2_STATUS}" == 'ok' ]; then
FILE_SOUND_IN=${FILE_SOUND_IN_OK}
EMOTION='happiness'
EMOTION_LEVEL=3
else
FILE_SOUND_IN=${FILE_SOUND_IN_ALERT}
EMOTION=''
EMOTION_LEVEL=
fi
HASH_TEXT=`echo ${TEXT_CO2} ${FILE_SOUND_IN} ${EMOTION} ${EMOTION_LEVEL} | md5sum | awk '{print $1}'`
if [ -e "${DIR_SOUND}/${HASH_TEXT}.${TYPE_SOUND}" ]; then
:
else
func_savevoice "${TMP_FILE}" "${TEXT_CO2}" ${EMOTION} ${EMOTION_LEVEL}
ffmpeg -y -loglevel quiet -i "${FILE_SOUND_IN}" -i "${TMP_FILE}" -filter_complex [0:a][1:a]concat=n=2:v=0:a=1 "${DIR_SOUND}/${HASH_TEXT}.${TYPE_SOUND}" >/dev/null
fi
rm "${FILE_SOUND_OUT}"
cp -p "${DIR_SOUND}/${HASH_TEXT}.${TYPE_SOUND}" "${FILE_SOUND_OUT}"
URL_BASE='http://【google-home-notifierサーバ】:8091/google-home-notifier/?text='
wget -q -O - "${URL_BASE}http://【自宅LAN内のウェブサーバ】/co2.${TYPE_SOUND}"
}
func_savestatus() {
echo $1 >${FILE_STATUS} # ok, alert
}
func_durationstatus() {
TMP_NOW=`date +"%s"`
TMP_MOD=`date +"%s" -r ${FILE_STATUS}`
if [ $((TMP_NOW - TMP_MOD)) -ge ${SEC_ALERT} ]; then
echo 1
else
echo 0
fi
}
if [ -e "${FILE_STATUS}" ]; then
CO2_STATUS=`cat ${FILE_STATUS} | awk '{print $1}'`
else
func_savestatus 'ok'
exit 0
fi
if [ ${CO2_NOW} -ge ${CO2_ALERT} ]; then
# CO2の値≧警戒値
if [ "${CO2_STATUS}" == 'ok' ]; then
# 警告スタート
func_savestatus 'alert'
func_voice 'alert' "${TEXT_CO2_ALERT}"
else
# 警告続行中
TMP_FLAG=`func_durationstatus`
if [ "${TMP_FLAG}" -eq 1 ]; then
func_savestatus 'alert'
func_voice 'alert' "${TEXT_CO2_ALERT}"
fi
fi
elif [ ${CO2_NOW} -le ${CO2_OK} ]; then
# CO2の値≦安心値
if [ "${CO2_STATUS}" == 'ok' ]; then
# OK継続中
:
else
# OKスタート
func_savestatus 'ok'
func_voice 'ok' "${TEXT_CO2_OK}"
fi
fi