RaspberryPi CO2 Cencor

# 概要

フルリモートでの執務がすっかり日常になった昨今。
一般的にオフィスは労働衛生基準が色々と整備されているのですが、自宅環境では「衛生基準？ 何それ？」という方々がほとんどではないだろうか？

先日、講義中にこんなに学生が眠くなるなんて……と二酸化炭素濃度を計測してみたら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、１分間に20回程度

とのこと。

僕が今これを書いている書斎（納戸かもしれない）が２畳なので、底面積3.24平方メートル × 天井高さを2.4mとして 7.776立方メートル。まぁモノが置いてあったり天井の一部が屋根に合わせて傾斜が付けられているので切りの良いところで7立方メートルとしよう。

- 一回の呼気の量 × １分間の呼吸回数 = 500ml * 20回/min = １分間の呼気の量は10リットル
- １分間の呼気の量 × 呼気中の二酸化炭素濃度 = 10リットル/min × 4% = １分ごとに増える二酸化炭素量は0.4リットル
- 7立方メートル = 7,000リットルなので、1分ごとに0.4 / 7,000 = 57ppmずつ二酸化炭素濃度が上昇することになる


仮にこの部屋で10分過ごすだけ、屋外の二酸化炭素濃度420ppm + 57ppm/min * 10min = 990ppm、つまり1000ppmの基準ギリギリになるということになる。きっつい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/42a22767-fce8-1227-5952-943512a9e9cd.png)

もちろん我々の居室は完全に密閉されたガラスの水槽ではないので実際は多少なりとも空気の出入りがあり（※）、こんな急上昇はしないし上限もあるはずだが人の呼気により閉鎖された空間の二酸化炭素濃度が上昇するイメージはつかめると思う。８畳ぐらいの部屋が欲しい。

※機械式換気装置が備えられている場合、0.5回/h以上の有効換気量が求められているようだ。


# 利用したもの
- Raspberry Pi
手元に転がっていたやつ。GPIOがあればだいたいどれでもOK。画像は後撮りした3だが、実際にはZero WHを利用。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/4341384f-15c9-9653-5d22-796639cf8e32.png)

- 32GBのMicroSDカード
手元に転がっていたやつ。32GBも必要ないけれど、逆にこれより小さいものを選択する経済的メリットもあまりない。小さすぎる（2GBとか）とOSが展開できない。

- 16x2 キャラクタディスプレイ（I2C接続可能な基板付き）
手元に転がっていたやつ。I2C接続可能な基盤が予め取り付けられたやつが便利。（画像下の４ピン出た基盤）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/65d74c16-8dea-4a62-1cef-718fa563fed0.png)


- 二酸化炭素濃度センサー
これはAmazonで新規購入。NDIR方式で計測できるMH-Z19のコンパチ品。3000円弱。
https://amzn.asia/d/aAvKmfZ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/54b6454b-ce28-69ee-25c5-b84e016bf13c.png)

※RaspberryPiの信号は3.3Vなので、キャラクタディスプレイや二酸化炭素濃度センサーの仕様が3.3Vで動作するものか要確認。Arduinoが5Vらしいので、世の中に混在してます。（上記のセンサーの様に両対応モデルもあったりする）

# 手順

## 物理的な接続

今回はI2Cでキャラクタディスプレイと、シリアル（UART）で二酸化炭素濃度センサーと接続した。（本当は二酸化炭素濃度センサーもI2Cで接続できるのでそれを試したかったが配線を分岐させるのが面倒くさくて）


```dummy:Raspberry PiのGPIO
 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、それと信号線が２本ずつである。

```
# 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)
```

結果としてこうなった。
```dummy:Raspberry PiのGPIOアサイン
                  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の起動まで）

今回はディスプレイやキーボードなどは接続せず、ヘッドレスで作業を進めた。
慣れてないばあいはそれらを繋いだ方が楽かもしれない。

1. Raspberry Pi Imagerを利用してMicroSDカードへOSを導入した
https://www.raspberrypi.com/software/
今回はデスクトップを利用することもないのでOSは`Raspberry Pi OS Lite 32bit`を利用している。
ヘッドレスで接続するのでイメージ焼き込み時にID/Passwordの設定のほか、hostnameや無線LANの定義などを入れておき、起動後にSSHで接続できるように準備しておいた。（設定を手動でテキストに押し込んでいた時代に比べるととても楽になった）

2. Raspberry PiへMicroSDカードを挿入して通電した。15分ほど待ち電源抜き差しで再起動。なぜだか初回起動時はそのまま接続可能な状態にならないことが多い。（ディスプレイとか繋いでいれば確認できるだろうけれど再起動した方が手っ取り早い）

3. 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`を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/1ec020b4-a08b-b7b1-f531-b7f2ce023efa.png)
`I5 I2C`を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/ead201c8-6d36-2ea1-636d-014e17c75070.png)
I2Cインタフェースを有効化するか訊いてくるので`<Yes>`を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/9a7a79c0-22d9-7c87-ebb0-5459e585785f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/d07a0108-ceee-d5d8-3b20-25f791ade8e0.png)
続いて`I6 Serial Port`を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/f7adea37-62ca-16ed-666b-8aa92d2daaca.png)
シリアルでシェルに接続できるようにするか訊いてくるのでこれは`<No>`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/eeb42afb-19da-7742-80e2-d1b97590d95c.png)
続いてシリアルポートハードウェアを有効化するか訊いてくるのでこちらを`<Yes>`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/62c5f344-806e-21f4-18ca-95dddb731397.png)
シリアルログインは無効化され、シリアルインタフェースは有効化される。これでOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/fdbb30de-bd4c-af23-69f3-b2f1dee00ffa.png)
最初の画面に戻って`<Finish>`。再起動を要求されるので応じる。

## I2C接続の確認
I2C-Toolsを導入してキャラクタディスプレイの接続を確認する。
```
apt install i2c-tools
```
導入できたら、今回接続したI2Cデバイスの検出を試してみる。
```
i2cdetect -y 1
```
恐らく下記の画像のように一つ検出されるはず。ここの`27`はキャラクタディスプレイのデバイスIDなので覚えておこう。（PythonからI2C制御する際にこのIDを指定する）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/71245c59-777c-ec62-db45-ab972c11f9c1.png)

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。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/8d9a0a17-e19a-a092-1abc-5761a6ae3af1.png)


今回、このコードはファイル分けせずに全てぶち込んでいるが、８割はLCDのコントロールのためのコードなので、可読性を考えてもファイルを分けた方が良いのは間違いない。

```python:main.py
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円均一で合うケースを探すのも良い。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554633/881b52be-510e-1d46-976a-0c619e214fa0.png)


# 参考資料
キャラクタディスプレイの接続やプログラム
https://osoyoo.com/2017/07/03/raspbery-pi3-drive-i2c-1602-lcd/

MH-Z19ライブラリの作者さま
https://qiita.com/UedaTakeyuki/items/c5226960a7328155635f