M5Dial + Bosch BME688で空気品質測定

初投稿です。
M5Stack社のM5Dialが手に入ったので、これを使って机上に置くガジェットを作ろうと画策中。その機能の一つとして部屋の空気品質測定をしようとしていて、Bosch BME688の動作確認をしたのでその覚え書きです。

# 使用機器・環境
- PlatformIO + VSCode
- [M5Dial](https://www.switch-science.com/products/9271)
- [ENV Pro(M5Stack用BME688搭載環境センサユニット)](https://www.switch-science.com/products/9274)

# Bosch BME688について
Boschの資料は[こちら](https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/products/unit/ENV%20Pro%20Unit/bst-bme688-ds000.pdf)。BME688で実現できる機能については[こちらのページ](https://iotnews.jp/core-technology/194458/)が参考になります。
ガスをセンシングして様々な分析ができる環境センサですが、ぶっちゃけどうやれば何ができるかわかっていません。資料によると以下の測定ができるとなっています。
- 揮発性有機化合物(VOC)
- 揮発性硫黄化合物(VSC)
- [空気質指数(IAQ)](https://ja.wikipedia.org/wiki/%E7%A9%BA%E6%B0%97%E8%B3%AA%E6%8C%87%E6%95%B0)
- CO2濃度
- 温度(-40～85℃)
- 湿度(0～100%)
- 気圧(300～1100hPa)

温度、湿度、気圧以外はガスセンサを利用して実現しているようです。細かい所は置いておいて、VOCやIAQの測定についてはBoschが提供しているBSEC2 Libraryを使うと取得することができるのでこれを利用しました。今後、特定のガス(火事、ペットの排泄物等)に反応する設定を試すかもしれません。

以下のソースコードを書き込んだM5Dialからの出力は以下のような感じです。
電源投入直後はガスセンサのスタビライゼーションが完了していないため、VOCやCO2の精度がイマイチですが、しばらく放置してaccuracyが3になるとちゃんとした出力になります。
![bme688_bsec2_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407566/3406fa14-41d3-d048-1aaf-117c9c327086.png)

# M5Dial
- 使用I2Cポート : PORT A(SCL: 15, SDA: 13)
- BME688 I2Cアドレス : 0x77 (ENV ProではSDOに+3.3Vが印加されており0x77になる)
- 使用ライブラリ
    - M5Unified
    - M5Dial
    - BME68x Sensor library
    - BSEC2 Software Library

# ソースコード
基本的にはBoschのBSEC2 Software Libraryのサンプル(以下スクショにある'basic')をそのまま使っていますが、
Bsec2オブジェクトに設定するセンサリストにBSEC_OUTPUT_CO2_EQUIVALENTとBSEC_BREATH_VOC_EQUIVALENTを追記し、CO2濃度[ppm]とVOC[ppm]を取得できるようにしています。
![vscode_library_bsec2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407566/57d870b4-371d-c904-8e5b-bbb56751aa70.png)

```C++:main.cpp
#include <Wire.h>
#include <M5Dial.h>
#include <bme68xLibrary.h>
#include <bsec2.h>

#define SERIAL_SPEED (115200)

#define PORTA_SCL_PIN (15)
#define PORTA_SDA_PIN (13)
#define PORTB_SCL_PIN (1)
#define PORTB_SDA_PIN (2)

#define BME688_ADDR (0x77)

Bsec2 envSensor;

bsecSensor sensorList[] = {
	BSEC_OUTPUT_IAQ,
	BSEC_OUTPUT_CO2_EQUIVALENT,
	BSEC_OUTPUT_BREATH_VOC_EQUIVALENT,
	BSEC_OUTPUT_RAW_TEMPERATURE,
	BSEC_OUTPUT_RAW_PRESSURE,
	BSEC_OUTPUT_RAW_HUMIDITY,
	BSEC_OUTPUT_RAW_GAS,
	BSEC_OUTPUT_STABILIZATION_STATUS,
	BSEC_OUTPUT_RUN_IN_STATUS};

void checkBsecStatus(Bsec2 bsec)
{
	if (bsec.status < BSEC_OK)
	{
		USBSerial.println("BSEC error code:" + String(bsec.status));
	}
	else if (bsec.status > BSEC_OK)
	{
		USBSerial.println("BSEC warning code:" + String(bsec.status));
	}

	if (bsec.sensor.status < BME68X_OK)
	{
		USBSerial.println("BME68X error code:" + String(bsec.sensor.status));
	}
	else if (bsec.sensor.status > BME68X_OK)
	{
		USBSerial.println("BME68X warning code:" + String(bsec.sensor.status));
	}
}

void newDataCallback(const bme68xData data, const bsecOutputs outputs, Bsec2 bsec)
{
	if (!outputs.nOutputs)
	{
		return;
	}

	USBSerial.println("BSEC outputs:\n\ttimestamp = " + String((int)(outputs.output[0].time_stamp / INT64_C(1000000))));
	for (uint8_t i = 0; i < outputs.nOutputs; i++)
	{
		const bsecData output = outputs.output[i];
		switch (output.sensor_id)
		{
		case BSEC_OUTPUT_IAQ:
			USBSerial.println("\tiaq = " + String(output.signal));
			USBSerial.println("\t\tiaq accuracy = " + String((int)output.accuracy));
			break;
		case BSEC_OUTPUT_CO2_EQUIVALENT:
			USBSerial.println("\tco2 = " + String(output.signal) + " ppm");
			USBSerial.println("\t\tco2 accuracy = " + String((int)output.accuracy));
			break;
		case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
			USBSerial.println("\tbreath voc = " + String(output.signal) + " ppm");
			USBSerial.println("\t\tbreath voc accuracy = " + String((int)output.accuracy));
			break;
		case BSEC_OUTPUT_RAW_TEMPERATURE:
			USBSerial.println("\ttemperature = " + String(output.signal) + " degC");
			break;
		case BSEC_OUTPUT_RAW_PRESSURE:
			USBSerial.println("\tpressure = " + String(output.signal / 100.0) + " hPa");
			break;
		case BSEC_OUTPUT_RAW_HUMIDITY:
			USBSerial.println("\thumidity = " + String(output.signal) + " %RH");
			break;
		case BSEC_OUTPUT_RAW_GAS:
			USBSerial.println("\tgas resistance = " + String(output.signal));
			break;
		case BSEC_OUTPUT_STABILIZATION_STATUS:
			USBSerial.println("\tstabilization status = " + String(output.signal));
			break;
		case BSEC_OUTPUT_RUN_IN_STATUS:
			USBSerial.println("\trun in status = " + String(output.signal));
			break;
		default:
			break;
		}
	}
}

void setup()
{
	auto cfg = M5.config();
	M5Dial.begin(false, false);
	USBSerial.begin(SERIAL_SPEED);
	Wire.begin(PORTA_SDA_PIN, PORTA_SCL_PIN);

	if (!envSensor.begin(BME688_ADDR, Wire))
	{
		checkBsecStatus(envSensor);
	}
	if (!envSensor.updateSubscription(sensorList, ARRAY_LEN(sensorList), BSEC_SAMPLE_RATE_LP))
	{
		checkBsecStatus(envSensor);
	}

	envSensor.attachCallback(newDataCallback);

	USBSerial.println("BSEC library version " +
					  String(envSensor.version.major) + "." + String(envSensor.version.minor) + "." + String(envSensor.version.major_bugfix) + "." + String(envSensor.version.minor_bugfix));
}

void loop()
{
	if (!envSensor.run())
	{
		checkBsecStatus(envSensor);
	}
}

```

```platformio.ini
[env:m5stack-stamps3]
platform = espressif32
board = m5stack-stamps3
framework = arduino
lib_deps = 
	m5stack/M5Dial@^1.0.2
	m5stack/M5Unified@^0.1.12
	boschsensortec/BME68x Sensor library@^1.1.40407
	boschsensortec/BSEC2 Software Library@^1.3.2200
platform_packages = framework-arduinoespressif32 @ https://github.com/bsergei/arduino-esp32.git#issue-8185
monitor_speed = 115200
```

# 参考資料
- [M5Stack社 ENV Pro Unit](https://docs.m5stack.com/en/unit/ENV%20Pro%20Unit)
- [GitHub Bosch BSEC2 Library](https://github.com/boschsensortec/Bosch-BSEC2-Library)
- [M5Dial Basic Library](https://github.com/m5stack/M5Dial/tree/master)
- [いろんな匂いを識別できる匂いセンサを作る (Bosch BME688利用)](https://qiita.com/ksasao/items/1e7cb98f29c0801c7d8b)

# あとがき
M5Stackとそのツールチェーンを使えばRTOS、ライブラリを組み合わせて100行程度でこの機能を実現できるって素晴らしいですね。