そろそろ冬なので部屋のCO2をRaspberry Piで計測してみた

こちらは [ジョブカン Advent Calendar 2023](https://qiita.com/advent-calendar/2023/jobcan) 20日目の記事になります。
担当はジョブカン採用管理で開発エンジニアをしています @maeda3net です。

https://qiita.com/advent-calendar/2023/jobcan

## はじめに
寒くなるにつれて窓を閉め切って換気をしなくなってきました。
換気が足りないと室内のCO2濃度が上がり、なんとなく息がしづらいような気がしてきますね。
そこでまずは自宅のCO2濃度を測定して可視化しようと思い立ち、Raspberry Pi + CO2センサーで構築してみたので構築手順から実際に計測したところまでを紹介します。

## 構成
* [Raspberry Pi 4B (4GBモデル)](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/)
* [SCD41 (CO2センサー)](https://shop.pimoroni.com/products/scd41-co2-sensor-breakout)
* リボンケーブル

センターを動かすマシンは~~誤~~家庭に何台かは転がっているRaspberry Piを使い、CO2センサーはpimoroniで探して扱いやすそうだったSCD41を購入しました。
また、Raspberry Piとセンサーの距離を離すために、余ってたリボンケーブルを使いました。

![image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621684/fd0070ab-f6a1-46b7-4872-db1bf64eb61d.jpeg)
こんな感じ

## 構築
Raspberry Pi自体のセットアップは割愛しますが、Raspberry Pi公式が配布している [Raspberry Pi Imager](https://www.raspberrypi.com/software/) が便利でセットアップが楽なのでおすすめです。
OS, Raspberry Piの種別, 書き込み先SDを選択の上、追加でWiFiやSSHの設定までやっておけば、起動後勝手にネットワークに接続して、こちらが事前に指定したホスト名でSSH可能な状態を作ることができます。（ddコマンドでイメージを焼き、モニタに接続してsshできるように設定したりしていたのが懐かしいです）

では、Raspberry Pi OS（Debian bookwormベース）でネットワークアクセスできるようにしたところからのスタートです。

### I2Cの有効化とセンサーの接続
今回使うセンサーはI2Cを使うため、Raspberry PiでI2Cを有効化して再起動します。
```shell
$ sudo raspi-config nonint do_i2c 0
$ sudo reboot
```

次にSCD41を接続します。
センサー側のピンはRaspberry PiのGPIOピンの1, 3, 5, 7, 9番と1対1で合うようにレイアウトされておりシンプルなため、配線で詰まることはないです。
ピンを繋げてRaspberry Piを起動したらすぐにデバイスが認識されます。

<!-- ここに接続した時の写真 -->

接続が完了したら、認識されているかの確認です。
下記コマンドで確認とれたらひとまず認識はされている状態と思っていいはずです。
```shell
$ i2cdetect -y 1 # アドレス0x62にi2cが認識されているのがわかる
```

### サンプルプログラムの実行
センサーを繋げたところで、次は pimoroni が用意してくれているライブラリとサンプルを動かしてみます。
https://github.com/pimoroni/scd4x-python

#### 準備
基本的にはREADMEにある通り、リポジトリをクローンして `install.sh` を実行すれば必要なライブラリのインストールや設定の変更などを行ってくれるように作られていますが、いくつかハマりポイントがありました。
下記は成功した手順を記載しますが、ハマった箇所については後述の [ハマったポイント](#ハマったポイント) にて書きます。（手を加えた箇所については本家リポジトリにそのうちフィードバックしたいですね）

1. リポジトリのクローン
    ```shell
    $ git clone https://github.com/pimoroni/scd4x-python
    $ cd scd4x-python
    ```
1. 仮想環境の作成および有効化
    ```shell
    $ python -m venv venv
    $ source ./venv/bin/activate
    ```
1. pyproject.toml の修正
    お好きなエディタで `pyproject.toml` 末尾にある `commands` の `raspi-config` に `sudo` を追加
1. インストールスクリプトの実行
    ```shell
    $ ./install.sh -p ./venv/bin/python
    ```
#### サンプル実行
リポジトリに `examples` ディレクトリがあり、ディレクトリ内のスクリプトを実行することですぐにセンサーを試せるようになっています。（とても親切）
`self-test.py`, `basic.py`, `bargraph.py` の3種類のスクリプトが用意されており、それぞれセンサーの動作チェック、 計測の実行、 ビジュアライズされた計測の実行といった内容になっています。

`self-test.py` の実行が成功すれば、他コマンドも問題なく動作するはずです。
`basic.py` を実行すると一定間隔でCO2濃度と湿度を計測してくれます。

## キャリブレーション
計測するにあたり、まずはセンサーのキャリブレーションを行いました。
今回は外気のCO2濃度を415PPM程度として調整しました。
[データシート](https://cdn.shopify.com/s/files/1/0174/1800/files/Sensirion_CO2_Sensors_SCD4x_Datasheet.pdf?v=1642503159) を参考に、3分以上の計測実施後、停止し強制キャリブレーションコマンドを実行する流れとなります。

SCD41 の basic.py とライブラリのコードを参考に適当に強制キャリブレーションのスクリプトを作りました。

```python
# force_recalibration.py

from scd4x import SCD4X, FORCE_RECALIBRATION
from datetime import datetime, timezone
import time

device = SCD4X(quiet=False)

# run mesure for 3mins
print("start periodic measurement")
device.start_periodic_measurement()

start = time.time()
while time.time() - 180 <= start:
    co2, temperature, relative_humidity, timestamp = device.measure()
    date = datetime.fromtimestamp(timestamp, timezone.utc)
    print(f"""
Time:   {date.strftime("%Y/%m/%d %H:%M:%S %Z")}
CO2:    {co2:.2f}PPM""")

device.stop_periodic_measurement()
print("stopped periodic measurement")

# force recalibration
print("start force recalibration")
response = device.rdwr(FORCE_RECALIBRATION, value=0x019f, response_length=1,  delay=400)
result = response
if result == 0xffff:
    print("force recalibration failed")
print(f"{result - 0x8000}")

print("done")
```


## 計測
キャリブレーションが完了したので計測タイムです。
実際に測ってみると、室内に人が居るか居ないかで大きくCO2濃度が変わることが可視化されて面白かったです。

下のグラフは帰宅時点からのCO2濃度の遷移を表しています。
1時間程度で300PPM程度上昇しているのがわかります。
うおォン 俺はまるで人間CO2発生器だ。

![CO2濃度遷移帰宅.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621684/2b21b211-0b8d-05b4-1160-e51ef758618b.png)


---


次に、ほとんど外出せずに引きこもった週末の遷移です。

8日は金曜日で出勤のため不在時の値が落ちています。9,10日がほぼ引きこもった日になります。
9日夕方に窓を開けての換気を行い、夜に少し外出をしました。
10日は全く外に出なかったため、徐々に濃度が上がっていき800PPMを大きく超える値を出していたりします。
試しに筋トレを軽くしてみたところ 1,000PPM を超えたりするなど、生活が反映されています。

![CO2濃度遷移週末.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621684/3a9ed053-fcfd-a266-f531-5b87bc4557c9.png)

---

さらに実験として風呂場で発泡入浴剤を使い濃度の変化を計測してみました。
![image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621684/3f4d7fab-6110-4092-26e3-e3079bbbb4d1.jpeg)
※ 実験に使用した入浴剤は計測後、スタッフが入浴に使用しました。

横軸がダメになっていますが、3分間で下記の通りの変化をしています。
結果は2,500PPMを大きく上回り、その後入浴剤が溶けるに従い徐々に濃度が下がっていきました。
入浴剤からちゃんとCO2が出ていることが確認できて満足です。

![CO2濃度遷移入浴剤.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621684/6b4a5467-5909-8daf-d867-1c89fd1b8c0f.png)


## ハマったポイント
### `install.sh` の実行時にエラーが発生する
`install.sh` についてはいくつかありました。

1. `sudo` をつけて実行しない
    README.md には `sudo` ありで実行するように書かれていますが、スクリプト内でuid: 0で動作させないように処理されています。
    このため `sudo` なしで実行する必要がありました。
    ```shell
    Script should not be run as root. Try './install.sh'
    ```
1. Pythonの仮想環境で実行する必要があった
    Debian Bookwormから [PEP668](https://peps.python.org/pep-0668/) に準拠したことで、基本的にグローバルに `pip install` することができなくなりました。
    この影響でライブラリのインストール時に下記エラーが発生しました。
    ```shell
    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.
        If you wish to install a non-Debian-packaged Python package,
        create a virtual environment using python3 -m venv path/to/venv.
        Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
        sure you have python3-full installed.
        For more information visit http://rptl.io/venv
    note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
    hint: See PEP 668 for the detailed specification.
    Installing stable library from pypi.
    ```

    これに対応するためにPythonの仮想環境を用意し、かつ `install.sh` の引数に実行時に利用する Python を仮想環境のものに明示することで期待通りの動作をしました。
1. 権限が足りなかった
    `install.sh` が `pyproject.toml` の `commands` を実行するような処理になっており、この時実行される `raspi-config` が `sudo` なしで指定されていたことで権限エラーが発生していました。
    スクリプトの直近の変更で `sudo` を要求しない形にされていましたが、このタイミングで動作しなくなったようなものに見えます。
    ```shell
    Setting up i2c... Backing up /boot/config.txt to /boot/config.preinstall-scd4x-yyyy-MM-dd-HH-mm-ss.txt
    /usr/bin/raspi-config: 298: cannot create /boot/firmware/config.txt.bak: Permission denied
    mv: cannot stat '/boot/firmware/config.txt.bak': No such file or directory
    sed: couldn't open temporary file /etc/modprobe.d/sed0xvVVD: Permission denied
    sed: couldn't open temporary file /etc/sedYy7AyP: Permission denied
    * Must be run as root - try 'sudo dtparam ...'
    ```
    こちらは当該箇所に `sudo` を追記することで対処しました。

### センサーは認識しているのにサンプルコードを実行すると `OSError` が発生する
`i2cdetect` コマンドで認識はされているのに、いざテストスクリプトや計測スクリプトを実行すると下記の通りエラーが発生する状況がありました。
```shell
Traceback (most recent call last):
  File "/home/xxxx/scd4x-python/examples/./self-test.py", line 4, in <module>
    device = SCD4X(quiet=False)
             ^^^^^^^^^^^^^^^^^^
  File "/home/xxxx/scd4x-python/venv/lib/python3.11/site-packages/scd4x/__init__.py", line 44, in __init__
    self.stop_periodic_measurement()
  File "/home/xxxx/scd4x-python/venv/lib/python3.11/site-packages/scd4x/__init__.py", line 132, in stop_periodic_measurement
    self.rdwr(STOP_PERIODIC_MEASUREMENT, delay=500)
  File "/home/xxxx/scd4x-python/venv/lib/python3.11/site-packages/scd4x/__init__.py", line 59, in rdwr
    self.bus.i2c_rdwr(msg_w)
  File "/home/xxxx/scd4x-python/venv/lib/python3.11/site-packages/smbus2/smbus2.py", line 658, in i2c_rdwr
    ioctl(self.fd, I2C_RDWR, ioctl_data)
OSError: [Errno 5] Input/output error
```
原因はシンプルでセンサーの接触不良により発生していました。
この時は仮でRaspberry Piとセンサーを接続しており、センサーとケーブルの接触部分がかなり緩かったのが問題でした。
たまたま手で固定しながら実行したところうまく動作し、接触不良に気づき解決しました。（これに1時間近く溶かしました）


## おわりに
ハードウェアの絡んだ遊びをすると、ソフトウェアだけの世界では普段お目にしない事象などでハマったりできるので、普段と違った楽しみ方ができてよかったです。
今回は計測にとどまりましたが、計測値をLCDに表示したり監視系ツールに投げて計測結果を溜めたり、換気アラートを作ったりと発展させていこうと考えています。

ちなみに今回はリボンケーブル使ったり、センサーとピンヘッダを半田付けしたりしましたが、pimoroniで販売されている [Breakout Garden Mini](https://shop.pimoroni.com/products/breakout-garden-mini-i2c-spi) という拡張ボードを使えば、センサーの再利用など含めてもうちょい楽にセットアップできたっぽいです。（半田付けが終わってから知りましたorz）


## お知らせ
DONUTSでは新卒中途問わず積極的に採用活動を行っています。
詳細はこちらをご確認ください。

https://www.donuts.ne.jp/recruit/


## 参考資料
* https://shop.pimoroni.com/products/scd41-co2-sensor-breakout
* https://github.com/pimoroni/scd4x-python
* https://cdn.shopify.com/s/files/1/0174/1800/files/Sensirion_CO2_Sensors_SCD4x_Datasheet.pdf?v=1642503159