二酸化炭素センサー (MH-Z19B) を Raspberry Pi で使ってみた 

# MH-Z19B を買ってみた

[二酸化炭素(CO2)濃度が高くなると、集中力が低下し、また頭痛や眠気の原因になる](https://www.watch.impress.co.jp/docs/review/review/1295050.html)ということが広くまことしやかに囁かれだしたのは新型コロナウイルスの影響で在宅勤務が増えてからだったと思います。[厚生労働省のページ](https://www.mhlw.go.jp/bunya/kenkou/seikatsu-eisei10/)では 1000 ppm 以下が推奨されていますが自分の部屋が現在何 ppm なのか測定する装置が無いとまったく分かりません。

ということでミーハーな私は二酸化炭素濃度を測定するセンサーが欲しいなと思ってたんですが今回遅ればせながら [MH-Z19B](https://www.winsen-sensor.com/sensors/co2-sensor/mh-z19b.html) を購入しました。AliExpress だと $20 程度で購入可能です。半田付けの必要がないピン付きのものが便利なようです。

# Python mh-z19 モジュール

MH-Z19B は単なるセンサーなので Raspberry Pi などに接続しないと実際に値を読むことはできません。しかし結論から言うと[ここ](https://github.com/UedaTakeyuki/mh-z19)に書いてある通りに接続し mh-z19 モジュールを使用すると簡単に二酸化炭素濃度を読むことができます。

https://github.com/UedaTakeyuki/mh-z19

![test.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360970/f97b1560-cd2c-0950-1472-3c9f54eeb690.jpeg)

コマンドラインからも簡単に値を読み込むことができますし

```
pi@test:~ $ sudo pip3 install mh-z19
pi@test:~ $ python3 -m mh_z19 --serial_console_untouched
{"co2": 1031}
```

Python のプログラムからももちろん値を読むことができます。

```
pi@test:~ $ python3
Python 3.7.3 (default, Jan 22 2021, 20:04:44)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mh_z19
>>> mh_z19.read(serial_console_untouched=True)
{'co2': 987}
```

ちなみに MH-Z19B は Raspberry Pi のシリアルインターフェイスを使用しているので上記を実行前に sudo raspi-config と実行し

```
3 Interface Options
--> P6 Serial Port Enable/disable shell messages on the serial connection
```

を選択してから

```
Would you like a login shell to be accessible over serial? --> No
Would you like the serial port hardware to be enabled? --> Yes
```

と答えてあげる必要があります。

上記設定を行うと /boot/config.txt の最後付近に以下の行が追加され Linux の再起動が促されます。

```
enable_uart=1
```

# Raspberry Pi のシリアル接続

先人の功績により思いの外簡単に二酸化炭素濃度を読み取ることができました。ここではもう少し遊ぶため(?)に上記モジュールが裏側でどんなことをしているのかを見ていくことにします。

まず図の黄色と青の線ですがそれぞれ TXD と RXD と書いてありますがこれはシリアル通信の送信と受信のための線になります。[User's Manual](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/ndir-co2-sensor/mh-z19b-co2-manual(ver1_6).pdf)を見ると UART (TXD) TTL Level data output と UART (RXD) TTL Level data input と書いてあります。

UART とはシリアル通信を行うためのプロトコルで[ここ](https://www.ingenious.jp/articles/howto/raspberry-pi-howto/gpio-uart/)とか[ここ](https://www.raspberrypi.org/documentation/configuration/uart.md)とか[ここ](https://www.electronicwings.com/raspberry-pi/raspberry-pi-uart-communication-using-python-and-c)が詳しいです。Raspberry Pi 側から見ると GPIO 14 が送信 GPIO 15 が受信に使用されます。

Linux から見るとシリアルデバイスは /dev/serial0 と /dev/serial1 として見えています。私の例では /dev/serial0 (UART0, PL011) を使用しています。

```
pi@test:~ $ ls -al /dev/serial*
lrwxrwxrwx 1 root root 5 May 12 02:01 /dev/serial0 -> ttyS0
lrwxrwxrwx 1 root root 7 May 12 02:01 /dev/serial1 -> ttyAMA0
```

/dev/serial0 は /dev/ttyS0 へのリンクとなっていますが /dev/ttyS0 は私のデフォルトの環境では以下のように見えています。[ここ](http://invalid-log.blogspot.com/2017/01/raspberry-pi-3.html)を見ると違う見え方をしているので Raspberry Pi の種類に依存するようです。ちなみに私は Raspberry Pi Zero WH を使用しています。

```
pi@test:~ $ ls -l /dev/ttyS0
crw-rw---- 1 root dialout 4, 64 May 12 02:27 /dev/ttyS0
```

Python mh-z19 モジュールの github には[一般ユーザーで実行する方法](https://github.com/UedaTakeyuki/mh-z19/wiki/How-to-use-without-root-permission.) が書かれています。

ここで注意すべきは /dev/ttyS0 になんらかの変更を加えても serial-getty@serial0 を(再)起動すると group が tty となり group の権限が書き込みのみとなってしまう点です。尚、私の環境では前述のようにデフォルトでは /dev/ttyS0 の group は dialout となっていました。

```
pi@test:~ $ sudo systemctl start serial-getty@serial0
pi@test:~ $ ls -l /dev/ttyS0
crw--w---- 1 root tty 4, 64 May 12 03:12 /dev/ttyS0  !!! <--- group が tty で group の書き込み権限なし
pi@test:~ $ sudo chmod g+r /dev/ttyS0                !!! <--- group の書き込み権限追加
pi@test:~ $ ls -l /dev/ttyS0
crw-rw---- 1 root tty 4, 64 May 12 03:12 /dev/ttyS0
pi@test:~ $ sudo systemctl stop serial-getty@serial0
pi@test:~ $ sudo systemctl start serial-getty@serial0  !!! <--- serial-getty@serial0 の再起動
pi@test:~ $ ls -l /dev/ttyS0
crw--w---- 1 root tty 4, 64 May 12 03:12 /dev/ttyS0  !!! <--- group が tty で group の書き込み権限なし
```

この状態では root 以外のユーザーではシリアルデバイスからの値の読み込みはできません。serial-getty@serial0 は mh-z19 モジュールを --serial_console_untouched オプションを指定せず実行すると内部で再起動されます。

なので、一般ユーザーで mh-z19 モジュールを使用するには serial-getty@serial0 が停止している状態でかつ /dev/ttyS0 に書き込み権限のあるグループに所属しているユーザーが --serial_console_untouched オプションを指定し実行する必要がありそうです。

```
pi@test:~ $ sudo systemctl status serial-getty@serial0
● serial-getty@serial0.service - Serial Getty on serial0
   Loaded: loaded (/lib/systemd/system/serial-getty@.service; disabled; vendor preset: enabled)
   Active: inactive (dead)   !!! <--- 無効
     Docs: man:agetty(8)
           man:systemd-getty-generator(8)
           http://0pointer.de/blog/projects/serial-console.html
pi@test:~ $ ls -l /dev/ttyS0
crw-rw---- 1 root dialout 4, 64 May 12 13:19 /dev/ttyS0
pi@test:~ $ egrep dialout /etc/group
dialout:x:20:pi
pi@test:~ $ python3 -m mh_z19 --serial_console_untouched
{"co2": 729}
```

serial-getty@serial0 は[ここ](http://0pointer.de/blog/projects/serial-console.html)とか[ここ](https://www.thegeekdiary.com/centos-rhel-7-how-to-configure-serial-getty-with-systemd/)が詳しいようです。コンソール接続時に仮想ターミナルの切り替えができるのですがこれを使用する必要が無ければ恐らく active にする必要は無いと思います。/dev/ttyS0 の group が tty となってしまっている場合は Linux を一旦再起動をすると元に戻ります。

追記: Raspberry Pi Configuration から Serial Console を Disable にすると /dev/ttyS0 の group が dialout になります。(serial-getty@serial0 を systemctl で disable にしても良いと思います。)

# Raspberry Pi でシリアルを使用するプログラムを書いてみる

モジュールを使用しないで値を読み込む場合は以下のようなプログラムとなります。

```
import serial

if __name__ == '__main__':

  serial_dev = '/dev/serial0'

  ser = serial.Serial(serial_dev, baudrate=9600)
  result=ser.write(b"\xff\x01\x86\x00\x00\x00\x00\x00\x79")
  s=ser.read(9)

  if len(s) >= 4 and s[0] == 0xff and s[1] == 0x86:
    print(s[2]*256 + s[3])
```

import serial ができない場合は pip3 で pyserial モジュールをインストールしてください。

```
pi@test:~ $ sudo apt install python3-pip
pi@test:~ $ sudo pip3 install pyserial
```

上記を実行するとモジュールを使用した際と同等の値を読むことができます。

```
pi@test:~ $ python3 ./my_mh_z19.py
644
pi@test:~ $ python3 -m mh_z19 --serial_console_untouched
{"co2": 644}
```

上記のプログラムがなにをしているかというとまず serial.Serial(serial_dev, baudrate=9600) で /dev/serial0 を 9600 bps(bit per second) でオープンしています。

次に ser.write(b"\xff\x01\x86\x00\x00\x00\x00\x00\x79") で [User's Manual](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/ndir-co2-sensor/mh-z19b-co2-manual(ver1_6).pdf) の 7 ページにかかれている  Read コマンド (FF 01 86 00 00 00 00 00 79) を MH-Z19B にシリアル経由で送信しています。次に s=ser.read(9) で MH-Z19B からの返答を 9 byte 分読み込んでいます。

ここで仮に FF 86 01 F4 00 00 00 00 85 という値が返ってきたとします。この 2 番めと 3 番めの 01 F4 が二酸化炭素濃度の値となります。16進数なので10進数になおしてあげると 1*256 + 254 = 500 ppm となります。if 文で s[0] と s[1] をチェックしているのは Start Byte と Command が正しい値となっているかの確認となります。

エラー処理をもう少ししっかりする場合には mh-z19 モジュールでされているように以下のようなプログラムを書いてあげると良いかもしれません。シリアル接続に関しても [User's Manual](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/ndir-co2-sensor/mh-z19b-co2-manual(ver1_6).pdf) の 6 ページに書かれている通り "Set serial port baud rate be 9600, data bytes have 8 bytes, stop byte has 1byte, parity byteisnull." でオープンしています。

```
import serial
import traceback

serial_dev = '/dev/serial0';

def connect_serial():
  return serial.Serial(serial_dev,
                        baudrate=9600,
                        bytesize=serial.EIGHTBITS,
                        parity=serial.PARITY_NONE,
                        stopbits=serial.STOPBITS_ONE,
                        timeout=1.0)

def mh_z19():
  try:
    ser = connect_serial()
    while 1:
      result=ser.write(b"\xff\x01\x86\x00\x00\x00\x00\x00\x79")
      s=ser.read(9)

      if len(s) >= 4 and s[0] == 0xff and s[1] == 0x86:
        return {'co2': s[2]*256 + s[3]}
        break
  except:
     traceback.print_exc()

if __name__ == '__main__':
  print(mh_z19())
```

上記のプログラムも二酸化炭素濃度の値を読めることを確認します。

```
pi@test:~/mh-z19b $ python3 ./my_mh_z19_2.py
{'co2': 595}
```

あとは任意のプログラムを用いてログをとったり警告を送ったりすることができます。では良い二酸化炭素濃度ライフをお過ごしください！

# 二酸化炭素濃度センサーの調整 (キャリブレーション)

一般的に二酸化炭素濃度センサーは外気にさらして値を 400 ppm (MH-Z19B の基準値) に調整する必要があります。これには外気に 20 分ほどさらし以下のコマンドを実行します。

```
$ python -m mh_z19 --zero_point_calibration
Call Calibration with ZERO point.
```

あるいは HD ピンを GND に 7 秒接続することでその時に測定された値が 400 ppm となります。

私の環境では調整前は外気にさらしていても 1500 ppm などを示しておりこれは正確な値を得るには必要なものとなっているようです。

参照:
[Intelligent Infrared CO2 Module (Model: MH-Z19B)](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf)
[CALIBRATION & detection range](https://github.com/UedaTakeyuki/mh-z19/wiki/CALIBRATION-&-detection-range)

#(おまけ) 二酸化炭素濃度センサーの種類について

二酸化炭素濃度センサーを選ぶ際ですが当然 MH-Z19B のような単なるセンサーではなく完成品を買うのが第一の選択肢になるはずです。ただ完成品は[ここ](https://www.watch.impress.co.jp/docs/review/review/1295050.html)とか[ここ](https://pc.watch.impress.co.jp/docs/column/ali/1298772.html)を見るとかなり玉石混交の状態らしく安いものは正確な値が測れなかったりするようです。ちょっと見た限りだと 10,000 円程度のものを買ったほうが良いという印象でした。

後者の記事には MH-Z19B のような赤外線を使った NDIR (Non Dispersive Infrared) 方式の二酸化炭素濃度センサーは消費電力が高いのでバッテリーの駆動は難しいとの指摘があります。Raspberry Pi Zero WH もそれなりに消費電力が高いのでモバイルバッテリーを用いた運用は数日が限界のようです。

電源の問題に関してはパソコンを起動中にのみ二酸化炭素濃度を測れれば良いというのであれば[ここ](https://qiita.com/YoshitakeKitamura/items/043e33f472c892c40925)にあるようにUSBシリアル変換モジュールを使用してパソコンに刺してしまうのは良いアイデアだと思いました。

センサーとしてはもう少し簡易的ですが [CCS811](https://www.switch-science.com/catalog/3298/) を使えばより消費電力が少なくて済むそうです。取得されたデータの比較が [ここ](https://ambidata.io/samples/temphumid/airqualitysensors/) でされていました。M5Stack と組み合わせると消費電力が少なくディプレイもついていて便利かもしれません。

MH-Z19B と MH-Z19C の違いについては [ここ](https://qiita.com/danzig1972/items/baf516ba10a0a11472ed) が詳しいです。両方とも読み取りの誤差は ± (50ppm + 5%) 程度のようです。[MH-Z14A](https://www.winsen-sensor.com/sensors/co2-sensor/mh-z14a.html) は ±(50ppm + 3%) なので単純に型番が大きいほうが性能が良いと言うわけでは無さそうです。MH-Z19C の方が電源投入してから測定値が安定するまでのプレヒート時間が MH-Z19C の 3 分に比べ 1 分と短くなっています。ぱっと見では実用上そこまで大きな違いは無さそうです。

当然完成品のほうが外観が綺麗で手軽に使えるデザインとなっています。値段が高いものだと外部に値を出力する方法を持つものもあるのかもしれません。しかし Raspberry Pi などとセンサーの組み合わせで作ると特に出力先を自由に決められます。配線も比較的少ないですし素晴らしい Python モジュールもあるため作っていて気軽に挑戦できると思います。