概要
フルリモートでの執務がすっかり日常になった昨今。
一般的にオフィスは労働衛生基準が色々と整備されているのですが、自宅環境では「衛生基準? 何それ?」という方々がほとんどではないだろうか?
先日、講義中にこんなに学生が眠くなるなんて……と二酸化炭素濃度を計測してみたら2000ppmを越えていた、なんてことが話題になっていたが、能率的な執務に環境の整備は欠かせない。
衛生基準のうち、トイレの数や更衣室なんて項目は自宅では一旦脇に置いておくとして、
- 室温
- 明るさ
- 二酸化炭素濃度
あたりは気にした方が良さそうである。
この中で最も縁遠く、目に見えない二酸化炭素濃度についてRaspberryPiとセンサ類を利用して計測してみたいと思う。
二酸化炭素の話
法的な基準と影響
働く上での衛生に関するルールのうち、二酸化炭素に関するものは以下になりそうだ。
労働安全衛生法(https://laws.e-gov.go.jp/law/347AC0000000057)
第二十三条事業者は、労働者を就業させる建設物その他の作業場について、通路、床面、階段等の保全並びに換気、採光、照明、保温、防湿、休養、避難及び清潔に必要な措置その他労働者の健康、風紀及び生命の保持のため必要な措置を講じなければならない。
事務所衛生基準規則(https://laws.e-gov.go.jp/law/347M50002000043)
第五条
事業者は、空気調和設備(空気を浄化し、その温度、湿度及び流量を調節して供給することができる設備をいう。以下同じ。)又は機械換気設備(空気を浄化し、その流量を調節して供給することができる設備をいう。以下同じ。)を設けている場合は、室に供給される空気が、次の各号に適合するように、当該設備を調整しなければならない。
二 当該空気中に占める一酸化炭素及び二酸化炭素の含有率が、それぞれ百万分の十以下(外気が汚染されているために、一酸化炭素の含有率が百万分の十以下の空気を供給することが困難な場合は、百万分の二十以下)及び百万分の千以下であること。
ここでいう、「二酸化炭素の含有率が百万分の千以下であること」=1,000ppmが二酸化炭素の基準であることがわかる。
二酸化炭素濃度の人への影響は当たる資料により表現の差異はあるが、概ね以下のようなところだった。
- 1,000ppm以上で認識能力(意思決定, 問題解決) などの精神運動機能への影響が表れる
- 2,000ppm以上で不快感、頭痛、めまいや吐き気などの肉体への影響が表れる
1000ppmを超えるなら換気を推奨するラインと言えるだろうか。
想定
※ 以下の値は「ざっくり」です。
- 屋外の二酸化炭素濃度は420ppm
- 人の呼気中の二酸化炭素濃度は4%(40,000ppm)
- 人の呼吸は一回500ml、1分間に20回程度
とのこと。
僕が今これを書いている書斎(納戸かもしれない)が2畳なので、底面積3.24平方メートル × 天井高さを2.4mとして 7.776立方メートル。まぁモノが置いてあったり天井の一部が屋根に合わせて傾斜が付けられているので切りの良いところで7立方メートルとしよう。
- 一回の呼気の量 × 1分間の呼吸回数 = 500ml * 20回/min = 1分間の呼気の量は10リットル
- 1分間の呼気の量 × 呼気中の二酸化炭素濃度 = 10リットル/min × 4% = 1分ごとに増える二酸化炭素量は0.4リットル
- 7立方メートル = 7,000リットルなので、1分ごとに0.4 / 7,000 = 57ppmずつ二酸化炭素濃度が上昇することになる
仮にこの部屋で10分過ごすだけ、屋外の二酸化炭素濃度420ppm + 57ppm/min * 10min = 990ppm、つまり1000ppmの基準ギリギリになるということになる。きっつい。
もちろん我々の居室は完全に密閉されたガラスの水槽ではないので実際は多少なりとも空気の出入りがあり(※)、こんな急上昇はしないし上限もあるはずだが人の呼気により閉鎖された空間の二酸化炭素濃度が上昇するイメージはつかめると思う。8畳ぐらいの部屋が欲しい。
※機械式換気装置が備えられている場合、0.5回/h以上の有効換気量が求められているようだ。
利用したもの
-
Raspberry Pi
手元に転がっていたやつ。GPIOがあればだいたいどれでもOK。画像は後撮りした3だが、実際にはZero WHを利用。
-
32GBのMicroSDカード
手元に転がっていたやつ。32GBも必要ないけれど、逆にこれより小さいものを選択する経済的メリットもあまりない。小さすぎる(2GBとか)とOSが展開できない。 -
16x2 キャラクタディスプレイ(I2C接続可能な基板付き)
手元に転がっていたやつ。I2C接続可能な基盤が予め取り付けられたやつが便利。(画像下の4ピン出た基盤)
-
二酸化炭素濃度センサー
これはAmazonで新規購入。NDIR方式で計測できるMH-Z19のコンパチ品。3000円弱。
https://amzn.asia/d/aAvKmfZ
※RaspberryPiの信号は3.3Vなので、キャラクタディスプレイや二酸化炭素濃度センサーの仕様が3.3Vで動作するものか要確認。Arduinoが5Vらしいので、世の中に混在してます。(上記のセンサーの様に両対応モデルもあったりする)
手順
物理的な接続
今回はI2Cでキャラクタディスプレイと、シリアル(UART)で二酸化炭素濃度センサーと接続した。(本当は二酸化炭素濃度センサーもI2Cで接続できるのでそれを試したかったが配線を分岐させるのが面倒くさくて)
3.3V ( 1) ( 2) 5V
GPIO2 ( 3) ( 4) 5V
GPIO3 ( 5) ( 6) GND
GPIO4 ( 7) ( 8) GPIO14 (TXD)
GND ( 9) (10) GPIO15 (RXD)
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND
GPIO22 (15) (16) GPIO23
3.3V (17) (18) GPIO24
GPIO10 (19) (20) GND
GPIO9 (21) (22) GPIO25
GPIO11 (23) (24) GPIO8
GND (25) (26) GPIO7
GPIO0 (27) (28) GPIO1
GPIO5 (29) (30) GND
GPIO6 (31) (32) GPIO12
GPIO13 (33) (34) GND
GPIO19 (35) (36) GPIO16
GPIO26 (37) (38) GPIO20
GND (39) (40) GPIO21
今回はとても結線がシンプル。
それぞれ5Vの電源とGND、それと信号線が2本ずつである。
# 16x2キャラクタLCD接続 (I2C経由)
LCD_SDA_PIN = 3 # GPIO 2 (ピン3)
LCD_SCL_PIN = 5 # GPIO 3 (ピン5)
LCD_VCC_PIN = 2 # 5V (ピン2またはピン4)
LCD_GND_PIN = 6 # GND (ピン6またはピン9)
# MH-Z19 二酸化炭素濃度センサー接続 (シリアル通信経由)
SENSOR_TX_PIN = 10 # GPIO 15 (ピン10, RXD)
SENSOR_RX_PIN = 8 # GPIO 14 (ピン8, TXD)
SENSOR_VCC_PIN = 4 # 5V (ピン4またはピン2)
SENSOR_GND_PIN = 9 # GND (ピン9またはピン6)
結果としてこうなった。
3.3V ( 1) ( 2) 5V <- LCD_VCC_PIN
LCD_SDA_PIN -> GPIO2 ( 3) ( 4) 5V <- SENSOR_VCC_PIN
LCD_SCL_PIN -> GPIO3 ( 5) ( 6) GND <- LCD_GND_PIN
GPIO4 ( 7) ( 8) GPIO14 (TXD) <- SENSOR_RX_PIN
SENSOR_GND_PIN -> GND ( 9) (10) GPIO15 (RXD) <- SENSOR_TX_PIN
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND
GPIO22 (15) (16) GPIO23
3.3V (17) (18) GPIO24
GPIO10 (19) (20) GND
GPIO9 (21) (22) GPIO25
GPIO11 (23) (24) GPIO8
GND (25) (26) GPIO7
GPIO0 (27) (28) GPIO1
GPIO5 (29) (30) GND
GPIO6 (31) (32) GPIO12
GPIO13 (33) (34) GND
GPIO19 (35) (36) GPIO16
GPIO26 (37) (38) GPIO20
GND (39) (40) GPIO21
シリアルについてはTXが送信、RXが受信なので、TXからRXへ、RXからTXへ接続してやる必要があるので注意。(同じ表記同士をつないではいけない)
二酸化炭素濃度センサー側に利用しないピンが余るが今回は使わないので気にしなくともよい。
前準備(Raspberry Piの起動まで)
今回はディスプレイやキーボードなどは接続せず、ヘッドレスで作業を進めた。
慣れてないばあいはそれらを繋いだ方が楽かもしれない。
-
Raspberry Pi Imagerを利用してMicroSDカードへOSを導入した
https://www.raspberrypi.com/software/
今回はデスクトップを利用することもないのでOSはRaspberry Pi OS Lite 32bit
を利用している。
ヘッドレスで接続するのでイメージ焼き込み時にID/Passwordの設定のほか、hostnameや無線LANの定義などを入れておき、起動後にSSHで接続できるように準備しておいた。(設定を手動でテキストに押し込んでいた時代に比べるととても楽になった) -
Raspberry PiへMicroSDカードを挿入して通電した。15分ほど待ち電源抜き差しで再起動。なぜだか初回起動時はそのまま接続可能な状態にならないことが多い。(ディスプレイとか繋いでいれば確認できるだろうけれど再起動した方が手っ取り早い)
-
SSHで接続する。ID/PasswordはOS焼き込み時に指定した値で。
Raspberry PiがDHCPでIPアドレスを取得するのでIPアドレスは不定となる。[raspberry piの名前].localで名前解決できればそこへSSHすればOK。
あるいはping 192.168.0.255
みたいにブロードキャストにping投げておいて、arp -a
でそれらしいやつを探してIPアドレスで接続してやる。
環境準備
raspi-config
今回はI2Cでキャラクタディスプレイと、シリアル(UART)で二酸化炭素濃度センサーと接続するので、最初にそれらのインタフェースを有効化してやる。
管理者権限でraspi-config
を実行。
3. Interface Options
を選択。
I5 I2C
を選択。
I2Cインタフェースを有効化するか訊いてくるので<Yes>
を選択。
続いてI6 Serial Port
を選択。
シリアルでシェルに接続できるようにするか訊いてくるのでこれは<No>
続いてシリアルポートハードウェアを有効化するか訊いてくるのでこちらを<Yes>
シリアルログインは無効化され、シリアルインタフェースは有効化される。これでOK。
最初の画面に戻って<Finish>
。再起動を要求されるので応じる。
I2C接続の確認
I2C-Toolsを導入してキャラクタディスプレイの接続を確認する。
apt install i2c-tools
導入できたら、今回接続したI2Cデバイスの検出を試してみる。
i2cdetect -y 1
恐らく下記の画像のように一つ検出されるはず。ここの27
はキャラクタディスプレイのデバイスIDなので覚えておこう。(PythonからI2C制御する際にこのIDを指定する)
PythonからI2Cを制御するためのライブラリを導入しておく。
apt install python3-smbus
シリアル接続の確認
続いてシリアル接続された二酸化炭素濃度センサーを確認していく。
前提ライブラリの導入
apt install swig liblgpio-dev
Pythonから二酸化炭素濃度センサーの値を取得するためのライブラリMH-Z19を導入。
https://pypi.org/project/mh-z19/
こちら、普通に環境全体に導入しようとするとエラーになる。
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
このライブラリの導入でPythonを利用している他システムにまで影響する可能性があるのでやめろ(と言われている認識)。
そのため、venvで環境を切り分けてその中に導入してやる。
また、先ほど導入したsmbusを参照させたいので、--system-site-packagesオプションを付けた。
mkdir co2
cd co2
python -m venv --system-site-packages .
venvを. bin/activate
してから、改めてmh-z19を導入。
(pypiのヘッダではpip install mh-z19
となっているが、本文の説明ではpip install mh_z19
となっている。謎。今回はとりあえず本文説明に準拠してmh_z19を指定した)
pip install mh_z19
mh_z19ライブラリの導入が完了したら、テストしてみる。
シリアルポートの利用は一般ユーザには権限がないので(たぶん)rootで行う。
# python -m mh_z19 --all
{"co2": 1692, "temperature": 28, "TT": 68, "SS": 0, "UhUl": 2304}
もし値が返ってこない(空配列になっている場合)は接続ミスを疑ってみる。
本当はrootではなく、適切なユーザをdialout
グループに追加してシリアルポート制御を許可してやる方が良いことは間違いない。
完成
ここまでで問題なく接続テストができれば基本的にはあとはPythonから制御してやればOK。
今回、このコードはファイル分けせずに全てぶち込んでいるが、8割はLCDのコントロールのためのコードなので、可読性を考えてもファイルを分けた方が良いのは間違いない。
import smbus
import time
import mh_z19
class LCD16x2:
# Define some device parameters
I2C_ADDR = 0x27 # I2C device address
WIDTH = 16 # Maximum characters per line
# Define some device constants
CHR = 1 # Mode - Sending data
CMD = 0 # Mode - Sending command
LINE_1 = 0x80 # LCD RAM address for the 1st line
LINE_2 = 0xC0 # LCD RAM address for the 2nd line
BACKLIGHT = 0x08 # On
#BACKLIGHT = 0x00 # Off
ENABLE = 0b00000100 # Enable bit
# Timing constants
E_PULSE = 0.0005
E_DELAY = 0.0005
def __init__(self):
# Open I2C interface
# self.bus = smbus.SMBus(0) # Rev 1 Pi uses 0
self.bus = smbus.SMBus(1) # Rev 2 Pi uses 1
self.lcd_init()
def __del__(self):
self.lcd_byte(0x01, self.CMD)
def lcd_init(self):
# Initialise display
self.lcd_byte(0x33,self.CMD) # 110011 Initialise
self.lcd_byte(0x32,self.CMD) # 110010 Initialise
self.lcd_byte(0x06,self.CMD) # 000110 Cursor move direction
self.lcd_byte(0x0C,self.CMD) # 001100 Display On,Cursor Off, Blink Off
self.lcd_byte(0x28,self.CMD) # 101000 Data length, number of lines, font size
self.lcd_byte(0x01,self.CMD) # 000001 Clear display
time.sleep(self.E_DELAY)
def lcd_byte(self, bits, mode):
# Send byte to data pins
# bits = the data
# mode = 1 for data
# 0 for command
bits_high = mode | (bits & 0xF0) | self.BACKLIGHT
bits_low = mode | ((bits<<4) & 0xF0) | self.BACKLIGHT
# High bits
self.bus.write_byte(self.I2C_ADDR, bits_high)
self.lcd_toggle_enable(bits_high)
# Low bits
self.bus.write_byte(self.I2C_ADDR, bits_low)
self.lcd_toggle_enable(bits_low)
def lcd_toggle_enable(self, bits):
# Toggle enable
time.sleep(self.E_DELAY)
self.bus.write_byte(self.I2C_ADDR, (bits | self.ENABLE))
time.sleep(self.E_PULSE)
self.bus.write_byte(self.I2C_ADDR,(bits & ~self.ENABLE))
time.sleep(self.E_DELAY)
def lcd_string(self, message, line):
# Send string to display
message = message.ljust(self.WIDTH," ")
self.lcd_byte(line, self.CMD)
for i in range(self.WIDTH):
self.lcd_byte(ord(message[i]),self.CHR)
def main():
# Main program block
while True:
data = mh_z19.read_all()
co2 = int((data.get('co2') - 200) * 0.9)
temperature = data.get('temperature')
# Send some test
lcd.lcd_string(f"CO2: {co2} ppm" ,lcd.LINE_1)
lcd.lcd_string(f"TMP: {temperature} degree C",lcd.LINE_2)
time.sleep(10)
if __name__ == '__main__':
lcd = LCD16x2()
try:
main()
except KeyboardInterrupt:
pass
finally:
del lcd
co2 = int((data.get('co2') - 200) * 0.9)
はどうも値が大きく出るので他の二酸化炭素濃度センサーに合わせて手動で補正してある。(センサの個体差もあると思うし、他の二酸化炭素濃度センサーが正しいのかも定かではない)
自分好みの切片と傾きを追及して欲しい。
cronで起動時に自動でスクリプトが動くようにしておけば、任意の部屋に設置して電源を繋げばそれだけで使えるようになる。venv環境でcron動かすのであれば、venvのbin内のPythonを指定してやれば良い。例えばこんな風。
@reboot /root/co2/bin/python /root/co2/main.py
最後に
実際に二酸化炭素濃度を計測してみると、すぐに労働衛生基準の1000ppmを超過することがわかる。冒頭の試算通りだ。もっとも、1,500ppm程度に至ると濃度の伸びは鈍化し、1,800~2,000ppmあたりで頭打ちになる。
なお、部屋に人が不在だとこの値は徐々に低下していくので、絶対的な値はズレがあるとしても相対的な指標としては受け入れて良さそう。小まめに喚起する動機づけとなる。
実際、数時間部屋にこもって作業していて、眠気を催したタイミングで確認すると1,500ppmを超えていることが多い。この残暑厳しい折ゆえ冷房全開で締め切っているため、窓とドアを開けて換気しながら作業した場合の眠気と比較していないのが惜しまれるところ。暑さによる命の危険がなくなったら試してみたい。
今回のコードでは単純に測定値を表示するにとどまっているが、せっかくなので一定基準を超えたらアラートを上げたり、クラウドに値を蓄積するなどした方が面白そうだ。
また、今回のセットは二酸化炭素濃度を計測する関係上、密閉するわけにもいかないが抜き身で転がしておくのも躊躇するところ。ちょうど良いケースを作るのが今後の山場になるかもしれない。身近なところではレゴが便利。100円均一で合うケースを探すのも良い。
参考資料
キャラクタディスプレイの接続やプログラム
https://osoyoo.com/2017/07/03/raspbery-pi3-drive-i2c-1602-lcd/
MH-Z19ライブラリの作者さま
https://qiita.com/UedaTakeyuki/items/c5226960a7328155635f