M5Stackをいじくりまわして遊ぶのは面白いのですが、なかなか使用用途が思いつきません。なにを作るか考えていましたが、結果的にやはり二酸化炭素濃度を表示するのがいちばん良さそう。
作っていて楽しく遊べて、さらに完成したら実用的に二酸化炭素濃度を表示してくれるので、換気の目安にもなって一挙両得な電子工作。

コロナ禍なので、お部屋の空気が気になる方が多いと思いますので、ちょっと電子工作で遊ぶついでに実用的なガジェットができれば嬉しいですね。

それと、M5Stackはコンパクトに収まっていますが、UNITのセンサーをつなげるケーブルが長くて邪魔で不満。
ということでM5Stackの裏にケーブルやセンサーを収めるスタッキングパーツを3Dプリンターで作ってみたので紹介します。
M5StackとSGP30センサーを使った結果・・・
SGP30センサーは二酸化炭素濃度ではあまり使えない・・

ここで、いきなりM5Stackで二酸化炭素濃度計測センサーを作ってわかった結果をお知らせすると、

⚠ 結論:TVOC/eCo2 SGP30センサーは二酸化炭素濃度の計測には適さない!
これから二酸化炭素濃度をM5Stackで計測する場合は、やはりMH-Z19センサーを使ったほうが良いと思います。
価格は3千円ほどとちょっと高めですが、こちらのセンサーのほうがより正確にCo2を計測できます。

SGP30センサーを2つ買ってしまったので、今回はSGP30を使って二酸化炭素濃度を計測するガジェットを作っていきます。
M5Stack FireとCore2を使用します
引き続きM5Stack FireとCore2を使用していきます。

M5Stackは最初からケースとディスプレイがあるので、便利ですね。
M5Stack FIRE
| MCU | ESP32(D0WDQ6-V3) |
|---|---|
| ディスプレイ | 2.0インチ320×210 (ILI9342) |
| フラッシュメモリ/PSRAM | 16MB / 4MB |
| GROVEポート | I2C,I/O,UART,I2C POGOPIN |
| ボタン | 電源,リセット,静電容量 x 3 |
| 機能 | スピーカー(1W-0928),RGB LEDx10,PMU(AXP192),MIC(BSBE3729),9軸DOF(MPU6886+BMM150),USB(CP2104) |
| MicroSDスロット | 最大16GB |
| バッテリー | 550mAh(3.7V) |
| サイズ | 54 x 54 x 20mm |
| 重量 | 52g |
3つのGROVEポートでさすだけ拡張性の高くLEGO互換パーツでUNITがつなげやすい
GROVEのI/OポートとUARTポートがついていてユニットを追加すると拡張性が高いM5Stack
POGO PIN接続の充電ドックも付属していて磁石でくっつく。
LEGO互換のLEGO PINでセンサーなどの拡張ユニットとつなげて設置しやすくなっている。
標準でWi-FiとBluetoothに対応・技適マークあり
M5Stack Core2

| MCU | ESP32(D0WDQ6-V3) |
|---|---|
| ディスプレイ | 2.0インチ タッチスクリーン(FT6336U) |
| フラッシュメモリ/PSRAM | 16MB / 8MB |
| GROVEポート | I2C |
| ボタン | 電源,リセット,静電容量 x 3 |
| 機能 | スピーカー(1W-0928),電源LED,振動モーター,RTC(BM8563),PMU(AXP192),MIC(SPM1423),6軸IMU(MPU6886),USB(CP2104) |
| MicroSDスロット | 最大16GB |
| バッテリー | 390mAh(3.7V) |
| サイズ | 54 x 54 x 16mm |
| 重量 | 52g |
M5Stack Coreの機能改良版 第二世代Core2
機能を改良した2020年新世代のM5Stack Core。
タッチスクリーン対応で、ディスプレイ下の3つのボタンもタッチタイプ。
標準でWi-FiとBluetoothに対応・技適マークあり
M5Stack Co2濃度計測スケッチのもと収集

二酸化炭素濃度を常時計測できるようにソースをかき集めて合体さえてちゃんと動くようにしてみます。
SGP30
SGP30センサーのユニットについては前回センサーをディスプレイに表示するまではやりましたので、こちらを参考にしてくださいね。

SGP30のキャリブレーション
こちらのキャリブレーションを参考に、ボタンを長押し(2回?)押したらキャリブレーションするようにしました。

ただ、キャリブレーションしてもあまり意味ないの?かなと思っていて、やはり12時間くらい電源を入れっぱなしにしたほうがセンサーが安定するようです。
安定した時に外に出して400PPMくらいの濃度でキャリブレーションすればうまくいくのかな??
M5StackのLCD表示
ディスプレイの表示はこちら↓に詳しく書いてありました。
M5Stack ボタン
M5Stackのボタン・Core2のタッチボタンを勉強するためにこちらの記事を参考にさせていただきました。
Fireの物理ボタン・Core2のタッチボタンともに同じスクリプトで動きました。
グラフ
コチラのグラフを参考にM5Stackのグラフ表示を参考にさせていただきました。
棒グラフはコチラの方のスケッチを参考にしました。↓

Beep
M5Stack Fireでこの通りにやったら、二酸化炭素濃度が高くなったときにそれほど大きくないビープ音が出るようになりました。

M5Stack Core2には対応していませんでした。現在調査中。
温湿度ENVセンサー
ENVセンサーユニットをM5Stack Core2に搭載していきます。
ライブラリはArduino IDEのツール > ライブラリを管理でBMP280を検索して以下の「Adafruit BMP280 Library」をインストールしました。
あと、SHT3Xも使いますのでインストールしておきましょう。
M5StackにENVセンサーとSGP30センサーを使う場合は、以下のようにライブラリがフォルダにコピーされますので、ない場合は手動でコピーしましょう。
Arduino IDEで新規ファイルを作ってスクリプトをそのままコピーしてコンパイルするとエラーになります。
Arduino IDEのLibrary保存場所の以下のパスにそれぞれありました。
Arduino\libraries\Adafruit_SGP30_Sensor
Arduino\libraries\arduino_261372\examples\Unit\ENVII_SHT30_BMP280
ENVセンサー(SHT3X.h,SHT3X.cpp)、SGP30センサー(Adafruit_SGP30.cpp,Adafruit_SGP30.h)の4つをコピーします。
データベース(InfluxDB)
InfluxDBは、ラズパイに計測データを保存しておくデータベース。なかなか便利でカッコいいグラフが簡単にできるのでオススメです。

InfluxDBを使う場合はこちら↑を参考にしてみてくださいね。

これらのスゴイ人が書いたプログラムを合体させてM5Stackに書き込んで二酸化炭素計測・グラフ表示に使います。
M5Stack Fire・Core2 二酸化炭素計測計測 スケッチ

Arduino IDEのFire/Core2&SGP30センサーを使ったスケッチを公開しておきますので、よかったら参考にしてくださいね。
M5Stack Fireに搭載した機能
Fireは寝室の3Dプリンターが置いてあるところに置く予定、ディスプレイには二酸化炭素計測値とグラフ・時計を表示させます。
- PIR 人感センサー:人が来たらディスプレイを点灯させる(UNIT購入)
- SGP30センサー:TVOC・eCo2計測(二酸化炭素計測)(UNIT購入)
- Beep:小さい音で警告を鳴らす。5000PPMで鳴らすようにした。
- RGB LED:Fireに装備されているLEDを流れるレインボーや警告時には赤点滅させる。
- InfluxDB+Timer:定期的にラズパイのデータベース(InfluxDB)に計測データを送信する。
- 計測値・時刻表示:ディスプレイに計測値と時刻を表示
- グラフ表示:ディスプレイに現在の二酸化炭素濃度をグラフで表示するようにした。
Beep音はSGP30センサーが正確ではなく、夜中にビービー鳴り出したので・・・5000PPMでアラートが鳴るようにしました。
M5Stack Fireは両サイドにLEDがあるので、通常は流れるレインボーにしました。
レインボーや赤点滅は↓Adafruit_NeoPixelをインストールしてスケッチ例をコピー・改造しただけです。
M5Stack FireのArduino IDEスケッチ

公開されているスケッチを組み合わせて合体させました。
かなり適当なスケッチですが、一応動いているので使えると思います。
#include <M5Stack.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <HTTPClient.h>
//Wi-Fi情報
#include <WiFi.h>
#define WIFI_SSID "●●●●●"
#define WIFI_PASSWORD "●●●●"
WiFiServer server(80);
//時計
struct tm timeInfo;//時刻を格納するオブジェクト
char s[20];//文字格納用
//TIMER
volatile int timeCounter1;
hw_timer_t *timer1 = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR onTimer1() {
portENTER_CRITICAL_ISR(&timerMux);
timeCounter1++;
portEXIT_CRITICAL_ISR(&timerMux);
}
//InfluxDB
#define PLACE "●●●●"
#define MEASUREMENT "air"
#define HOST "M5Stack"
const char* influxUrl = "http://192.168.0.1:8086/write?db=●●●●";
const char* influxUser = "root";
const char* influxPassword = "●●●●●";
//サーバー ラズパイ4に接続
void postToInfluxDB(int co2, int tvoc) {
String influxData = MEASUREMENT; //MEASUREMENT(table)
influxData += ",place=" PLACE ",host=" HOST;
influxData += " co2=";
influxData += co2;
influxData += ",tvoc=";
influxData += tvoc;
HTTPClient http;
http.begin(influxUrl);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
http.setAuthorization(influxUser, influxPassword);
int httpCode = http.POST(influxData);
http.end();
Serial.print( influxData + "\n");
}
//SGP30 Co2
#include "Adafruit_SGP30.h"
Adafruit_SGP30 sgp;
//RGB LED
#include <Adafruit_NeoPixel.h>
#define M5STACK_FIRE_NEO_NUM_LEDS 10
#define M5STACK_FIRE_NEO_DATA_PIN 15
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(M5STACK_FIRE_NEO_NUM_LEDS, M5STACK_FIRE_NEO_DATA_PIN, NEO_GRB + NEO_KHZ800);
int Brightness = 10;//明るさ
//Beep
#define GPIO_PIN 25
uint32_t beep_last_time = 0;
uint8_t beep_volume = 50; //min 1, max 255
uint32_t beep_total_time = 0;
//graph
int Ycoordinate;
int height;
TFT_eSprite graph1 = TFT_eSprite(&M5.Lcd);
int j = 0;
int z = 0;
void setup() {
pixels.begin();//RGB LED
beep_total_time = millis();//Beep
pinMode(36, INPUT);//PIR Sensor
//Co2
M5.begin(true, false, true, true);
sgp.begin();
//M5.Lcd.drawString("TVOC:", 50, 40, 4);
//M5.Lcd.drawString("eCO2:", 50, 80, 4);
Serial.print("Found SGP30 serial #");
Serial.print(sgp.serialnumber[0], HEX);
Serial.print(sgp.serialnumber[1], HEX);
Serial.println(sgp.serialnumber[2], HEX);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("WiFi connecting");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println(" connected:");
server.begin();
Serial.println(WiFi.localIP());
//TIME NTPの設定
configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
//TIMER
timer1 = timerBegin(0, 80, true);
timerAttachInterrupt(timer1, &onTimer1, true);
timerAlarmWrite(timer1, 300000000, true);//10000000=10秒
timerAlarmEnable(timer1);
//graph
graph1.setColorDepth(8);
graph1.createSprite(320, 150);
graph1.fillSprite(BLACK);
}
void loop() {
//static int pixelNumber=0;// = random(0, M5STACK_FIRE_NEO_NUM_LEDS - 1);
//RGB LED
if (sgp.eCO2 > 1500){//緊急用 点滅
theaterChase(pixels.Color(127, 0, 0), 3); // Red
}else{//ランダムにレインボーカラー
rainbowCycle(1);
}
//Wifi
WiFiClient client = server.available();
if (client) {
client.println("Hello World!");
client.stop();
}
//時計
getLocalTime(&timeInfo);
sprintf(s, " %02d/%02d %02d:%02d", timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min);
//Serial.println(s);//時間
//SGP30 eCo2
if (! sgp.IAQmeasure()) {
Serial.println("Measurement failed");
return;
}
//Co2 キャリブレーション
M5.update();
if (M5.BtnA.wasPressed()) {
Serial.println("BtnUP.isPressed()");
z += 1;
if (z = 2){//2回タップした時にキャリブレーションする
sgp30_calib(sgp.eCO2, sgp.TVOC);
}
}
//Beep
if (sgp.eCO2 > 5000){//警告を鳴らす
Serial.print("over");
dacWrite(GPIO_PIN, 0);
delay(1);
dacWrite(GPIO_PIN, beep_volume);
delay(1);
}
//PIR seonsor 赤外線で人感
if(digitalRead(36)==1){
Serial.println(" value: 1");
M5.Lcd.setBrightness(180);//バックライト0~255
delay(5000);
}else{
Serial.println(" value: 0");
M5.Lcd.setBrightness(0);
}
//TIMER
if (timeCounter1 > 0) {
portENTER_CRITICAL(&timerMux);
timeCounter1--;
portEXIT_CRITICAL(&timerMux);
//InfluxDB
postToInfluxDB(sgp.eCO2, sgp.TVOC);
}
//数値の表示される部分が桁数増えると残像残るので定期的にクリアする
M5.Lcd.fillRect(0, 0, 320, 29, BLACK);
M5.Lcd.fillRect(0, 31, 320, 48, BLACK);
M5.Lcd.fillRect(0, 91, 320, 28, BLACK);
M5.Lcd.drawNumber(sgp.eCO2, 90, 30, 7);
M5.Lcd.drawString("ppm", 250, 50, 4);
Serial.print("TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");
Serial.print("eCO2 "); Serial.print(sgp.eCO2); Serial.println(" ppm");
M5.Lcd.drawString(s, 5, 0, 4);
M5.Lcd.drawNumber(sgp.TVOC, 230, 0, 4);
M5.Lcd.drawString("ppb", 275, 0, 4);
M5.Lcd.drawLine(0, 25, 320, 25, LIGHTGREY);
M5.Lcd.drawLine(0, 80, 320, 80, LIGHTGREY);// 下に温度・湿度を表示させる
//graph
//四角形の高さ Co2計測値 * 150(グラフの最大高さ) / Co2最大値
height = (sgp.eCO2 * 150) / 3000;
//点のy位置 151(Display一番下から1Px上) - 四角形の高さ
Ycoordinate = int(151 - height);
graph1.pushSprite(0, 90);//グラフ表示範囲 横・縦
Serial.print(Ycoordinate);Serial.print("-");Serial.println(height);
if (j < 33) {
j += 1;
print_graph(sgp.eCO2, j, Ycoordinate, height);
delay(1000); // wait so things do not scroll too fast
graph1.scroll(-1, 0);
}
if ( j >= 33) {
print_graph(sgp.eCO2, j, Ycoordinate, height);
graph1.scroll(-10, 0); // scroll graph 10 pixel left, 0 up/down
}
//TIMER 一定時間ごとにInfluxDBにデータ送信
if (timeCounter1 > 0) {
portENTER_CRITICAL(&timerMux);
timeCounter1--;
portEXIT_CRITICAL(&timerMux);
//InfluxDB
postToInfluxDB(sgp.eCO2, sgp.TVOC);
}
dacWrite(GPIO_PIN, 0);//Beep OFF
}
//graph表示 Co2濃度ごとに色分けする
void print_graph(int co2, int j, int y, int h){
if(co2 < 600){
graph1.fillRect(j * 9, y, 9, h, CYAN);//点のx位置,点のy位置,四角形の幅,四角形の高さ
}else if(co2 > 600 && co2 < 800){
graph1.fillRect(j * 9, y, 9, h, GREEN);
}else if(co2 > 800 && co2 < 1000){
graph1.fillRect(j * 9, y, 9, h, YELLOW);
}else if(co2 > 1000 && co2 < 1400){
graph1.fillRect(j * 9, y, 9, h, TFT_ORANGE);
}else if(co2 > 1400 && co2< 1900){
graph1.fillRect(j * 9, y, 9, h, TFT_MAGENTA);
}else if(co2 > 1900){
graph1.fillRect(j * 9, y, 9, h, TFT_RED);
}
}
//SGP30 キャリブレーション
void sgp30_calib(int co2, int tvoc) {
uint16_t TVOC_base;
uint16_t eCO2_base;
uint8_t calibrationNum = 0;
if (SPIFFS.begin(true)) {
Serial.println("calibration mode");
calibrationNum = 60;
} else {
Serial.println("SPIFFS Mount Failed");
}
int i = calibrationNum;
long last_millis = 0;
Serial.print("Sensor init\n");
while (i > 0) {
if (millis() - last_millis > 1000) {
last_millis = millis();
i--;
if (sgp.IAQmeasure()) {
Serial.printf("%d:", calibrationNum - i);
Serial.print("eCO2 "); Serial.print(co2); Serial.print(" ppm\t");
Serial.print("TVOC "); Serial.print(tvoc); Serial.println(" ppb");
if (sgp.getIAQBaseline(&eCO2_base, &TVOC_base)) {
// 現在のbaselineを表示
Serial.print("eCO2: 0x");
Serial.print(eCO2_base, HEX);
Serial.print(" TVOC: 0x");
Serial.println(TVOC_base, HEX);
}
}
}
}
// キャリブレーションモードならばbalselineを設定(固定)してSPIFFSに書き込む
sgp.setIAQBaseline(eCO2_base, TVOC_base);
spiffsWriteBaseline(eCO2_base, TVOC_base);
Serial.println("done");
}
void spiffsWriteBaseline(uint16_t eCO2_new, uint16_t TVOC_new) {
// eCO2とTVOCのbaseline値を書き込む
// uint16_tをuint8_tに分割する
File fp = SPIFFS.open("/baseline", FILE_WRITE);
uint8_t baseline[4] = { (eCO2_new & 0xFF00) >> 8, eCO2_new & 0xFF, (TVOC_new & 0xFF00) >> 8, TVOC_new & 0xFF };
fp.write(baseline, 4);
fp.close();
Serial.printf("SPIFFS Wrote %x %x %x %x\n", baseline[0], baseline[1], baseline[2], baseline[3]);
}
//LED Adafruit_NeoPixel
// Slightly different, this makes the rainbow equally distributed throughout
void rainbowCycle(uint8_t wait) {
pixels.setBrightness(5);
uint16_t i, j;
for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
for(i=0; i< pixels.numPixels(); i++) {
pixels.setPixelColor(i, Wheel(((i * 256 / pixels.numPixels()) + j) & 255));
}
pixels.show();
delay(wait);
}
}
//Theatre-style crawling lights.
void theaterChase(uint32_t c, uint8_t wait) {
pixels.setBrightness(200);
for (int j=0; j<10; j++) { //do 10 cycles of chasing
for (int q=0; q < 3; q++) {
for (uint16_t i=0; i < pixels.numPixels(); i=i+3) {
pixels.setPixelColor(i+q, c); //turn every third pixel on
}
pixels.show();
delay(wait);
for (uint16_t i=0; i < pixels.numPixels(); i=i+3) {
pixels.setPixelColor(i+q, 0); //turn every third pixel off
}
}
}
}
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if(WheelPos < 85) {
return pixels.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if(WheelPos < 170) {
WheelPos -= 85;
return pixels.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return pixels.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
スケッチのフォルダには、Adafruit_SGP30.h・Adafruit_SGP30.cppファイルが必要です。ない場合はツール > ライブラリを管理から「Adafruit_SGP30」を検索してインストールしてください。
M5Stack Core2に搭載した機能
続いてCore2のほうは、SGP30センサーと温湿度計測のENVセンサーをつなげて作りました。
- SGP30センサー:TVOC・eCo2計測(二酸化炭素計測)(UNIT購入)
- ENVセンサー:温湿度・大気圧を計測(UNIT購入)
- Beep:まだちゃんと動いていません。現在調査中
- InfluxDB+Timer:定期的にラズパイのデータベース(InfluxDB)に計測データを送信する。
- 計測値・時刻表示:ディスプレイに計測値と時刻を表示
- グラフ表示:ディスプレイに現在の二酸化炭素濃度をグラフで表示するようにした。
温湿度センサーのほかは、M5Stack Fireと一緒ですが、Beepがまだ鳴らず、LEDも1つしかないので、ちょっとさみしい感じ。
M5Stack Core2のスケッチ
M5Stack Fireとほぼ同じですが、ENVセンサーを追加して、RGB LEDが無いソースになっています。
#include <M5Core2.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <HTTPClient.h>
#include "Adafruit_SGP30.h"
//ENV SHT30
#include "Adafruit_Sensor.h"
#include <Adafruit_BMP280.h>
#include "SHT3X.h"
SHT3X sht30;
Adafruit_BMP280 bme;
float tmp = 0.0;
float hum = 0.0;
float pressure = 0.0;
//Wi-Fi情報
#include <WiFi.h>
#define WIFI_SSID "●●●●"
#define WIFI_PASSWORD "●●●●"
WiFiServer server(80);
//時計
struct tm timeInfo;
char ts[20];//文字格納用
//TIMER
volatile int timeCounter1;
hw_timer_t *timer1 = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR onTimer1() {
portENTER_CRITICAL_ISR(&timerMux);
timeCounter1++;
portEXIT_CRITICAL_ISR(&timerMux);
}
//InfluxDB
#define PLACE "●●●●"
#define MEASUREMENT "air"
#define HOST "M5Stack"
const char* influxUrl = "http://192.168.0.1:8086/write?db=●●●●";
const char* influxUser = "root";
const char* influxPassword = "●●●●";
//サーバー ラズパイ4に接続
void postToInfluxDB(int co2, int tvoc, float temp, float hum) {
String influxData = MEASUREMENT; //MEASUREMENT(table)
influxData += ",place=" PLACE ",host=" HOST;
influxData += " co2=";
influxData += co2;
influxData += ",tvoc=";
influxData += tvoc;
influxData += ",temp=";
influxData += temp;
influxData += ",humi=";
influxData += hum;
HTTPClient http;
http.begin(influxUrl);
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
http.setAuthorization(influxUser, influxPassword);
int httpCode = http.POST(influxData);
http.end();
Serial.print( influxData + "\n");
}
Adafruit_SGP30 sgp;
int i = 15;
long last_millis = 0;
//graph
int Ycoordinate;
int height;
TFT_eSprite graph1 = TFT_eSprite(&M5.Lcd);
int j = 0;
int z = 0;
void setup() {
M5.begin(true, false, true, true);
Wire.begin();
sgp.begin();
//ENV SHT30
while (!bme.begin(0x76)) {
Serial.println("Could not find a valid BMP280 sensor, check wiring!");
}
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("WiFi connecting");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println(" connected:");
server.begin();
Serial.println(WiFi.localIP());
//TIME NTPの設定
configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
//TIMER
timer1 = timerBegin(0, 80, true);
timerAttachInterrupt(timer1, &onTimer1, true);
timerAlarmWrite(timer1, 300000000, true);//10000000=10秒
timerAlarmEnable(timer1);
//graph
graph1.setColorDepth(8);
graph1.createSprite(320, 120);
graph1.fillSprite(BLACK);
graph1.pushSprite(0, 120);
}
void loop() {
//Wifi
WiFiClient client = server.available();
//時計
getLocalTime(&timeInfo);
sprintf(ts, " %02d/%02d %02d:%02d", timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min);
Serial.println(ts);//時間
//SGP30 eCo2
if (! sgp.IAQmeasure()) {
Serial.println("Measurement failed");
return;
}
//Co2 キャリブレーション
M5.update();
if (M5.BtnA.wasPressed()) {
Serial.println("BtnUP.isPressed()");
z += 1;
if (z = 2){//2回タップした時にキャリブレーションする
sgp30_calib(sgp.eCO2, sgp.TVOC);
}
}
//ENV
pressure = bme.readPressure();
if (sht30.get() == 0) {
tmp = sht30.cTemp;
hum = sht30.humidity;
}
//Serial.printf("Temperatura: %2.2f*C Humedad: %0.2f%% Pressure: %0.2fPa\r\n", tmp, hum, pressure);
//数値の表示される部分が桁数増えると残像残るので定期的にクリアする
M5.Lcd.fillRect(0, 0, 320, 29, BLACK);
M5.Lcd.fillRect(0, 31, 320, 48, BLACK);
M5.Lcd.fillRect(0, 91, 320, 28, BLACK);
M5.Lcd.drawNumber(sgp.eCO2, 90, 30, 7);
M5.Lcd.drawString("ppm", 250, 50, 4);
Serial.print("TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");
Serial.print("eCO2 "); Serial.print(sgp.eCO2); Serial.println(" ppm");
M5.Lcd.drawString(ts, 5, 0, 4);
M5.Lcd.drawNumber(sgp.TVOC, 230, 0, 4);
M5.Lcd.drawString("ppb", 275, 0, 4);
M5.Lcd.drawFloat(tmp, 2, 40, 90, 4);
M5.Lcd.drawString("C", 110, 90, 4);
M5.Lcd.drawFloat(hum, 2, 180, 90, 4);
M5.Lcd.drawString("%", 250, 90, 4);
M5.Lcd.drawLine(0, 25, 320, 25, LIGHTGREY);
M5.Lcd.drawLine(0, 80, 320, 80, LIGHTGREY);// 下に温度・湿度を表示させる
M5.Lcd.drawLine(0, 119, 320, 119, LIGHTGREY);// 以下、グラフ表示
//graph
//四角形の高さ Co2計測値 * 120(グラフの最大高さ) / Co2最大値
height = (sgp.eCO2 * 120) / 3000;
//点のy位置 121(Display一番下から1Px上) - 四角形の高さ
Ycoordinate = int(121 - height);
graph1.pushSprite(0, 120);//グラフ表示範囲 横・縦
//Serial.print(Ycoordinate);Serial.print("-");Serial.println(height);
if (j < 33) {
j += 1;
print_graph(sgp.eCO2, j, Ycoordinate, height);
delay(1000); // wait so things do not scroll too fast
graph1.scroll(-1, 0);
}
if ( j >= 33) {
print_graph(sgp.eCO2, j, Ycoordinate, height);
graph1.scroll(-10, 0); // scroll graph 10 pixel left, 0 up/down
}
//TIMER 一定時間ごとにInfluxDBにデータ送信
if (timeCounter1 > 0) {
portENTER_CRITICAL(&timerMux);
timeCounter1--;
portEXIT_CRITICAL(&timerMux);
//InfluxDB
postToInfluxDB(sgp.eCO2, sgp.TVOC, tmp, hum );
}
//dacWrite(GPIO_PIN, 0);//Beep OFF
delay(1000);
}
//graph表示 Co2濃度ごとに色分けする
void print_graph(int co2, int j, int y, int h){
if(co2 < 600){
graph1.fillRect(j * 9, y, 9, h, CYAN);//点のx位置,点のy位置,四角形の幅,四角形の高さ
}else if(co2 > 600 && co2 < 800){
graph1.fillRect(j * 9, y, 9, h, GREEN);
}else if(co2 > 800 && co2 < 1000){
graph1.fillRect(j * 9, y, 9, h, YELLOW);
}else if(co2 > 1000 && co2 < 1400){
graph1.fillRect(j * 9, y, 9, h, TFT_ORANGE);
}else if(co2 > 1400 && co2< 1900){
graph1.fillRect(j * 9, y, 9, h, TFT_MAGENTA);
}else if(co2 > 1900){
graph1.fillRect(j * 9, y, 9, h, TFT_RED);
}
}
//SGP30 キャリブレーション
void sgp30_calib(int co2, int tvoc) {
uint16_t TVOC_base;
uint16_t eCO2_base;
uint8_t calibrationNum = 0;
if (SPIFFS.begin(true)) {
Serial.println("calibration mode");
calibrationNum = 60;
} else {
Serial.println("SPIFFS Mount Failed");
}
int i = calibrationNum;
long last_millis = 0;
Serial.print("Sensor init\n");
while (i > 0) {
if (millis() - last_millis > 1000) {
last_millis = millis();
i--;
if (sgp.IAQmeasure()) {
Serial.printf("%d:", calibrationNum - i);
Serial.print("eCO2 "); Serial.print(co2); Serial.print(" ppm\t");
Serial.print("TVOC "); Serial.print(tvoc); Serial.println(" ppb");
if (sgp.getIAQBaseline(&eCO2_base, &TVOC_base)) {
// 現在のbaselineを表示
Serial.print("eCO2: 0x");
Serial.print(eCO2_base, HEX);
Serial.print(" TVOC: 0x");
Serial.println(TVOC_base, HEX);
}
}
}
}
// キャリブレーションモードならばbalselineを設定(固定)してSPIFFSに書き込む
sgp.setIAQBaseline(eCO2_base, TVOC_base);
spiffsWriteBaseline(eCO2_base, TVOC_base);
Serial.println("done");
}
void spiffsWriteBaseline(uint16_t eCO2_new, uint16_t TVOC_new) {
// eCO2とTVOCのbaseline値を書き込む
// uint16_tをuint8_tに分割する
File fp = SPIFFS.open("/baseline", FILE_WRITE);
uint8_t baseline[4] = { (eCO2_new & 0xFF00) >> 8, eCO2_new & 0xFF, (TVOC_new & 0xFF00) >> 8, TVOC_new & 0xFF };
fp.write(baseline, 4);
fp.close();
Serial.printf("SPIFFS Wrote %x %x %x %x\n", baseline[0], baseline[1], baseline[2], baseline[3]);
}
これで、M5Stackに二酸化炭素濃度を計測させて、ディスプレイに数値とグラフ表示するスケッチが完成しました。

続いて、センサーやケーブルをまとめるスタッキングケースを紹介します。
M5Stack スタッキング用3Dデータ

M5Stackを二酸化炭素計測専用にするために3Dプリンターでケーブルやセンサーを中に収納するケースを作っていきます。
M5Stack Unitのケーブルがグチャグチャしてイヤ!
M5Stack本体はコンパクトで良いのですが、ユニットのセンサーなどのパーツを繋ぐケーブルがかなり長くて収まり悪すぎます・・・
なので、収まりが良くなるように3Dプリンターでスタッキングパーツを作っていきます。
まずは、M5Stack用の3Dデータを物色していきます。
M5Stack用スタッキング3Dデータ
こちらのM5Stackの下にスタッキングできる3Dデータを使わせていただきます。

LEGO PIN
ジョイントに使うLEGOのピンは以下のがM5Stackとピッタリで使えます。

ロングな3段のピン↓もありましたが、こちらはちょっとキツかった。

M5Stack Fireのスタッキングパーツ
ほんとうは、ユニットのケーブルを短くしようと思いましたが、よく考えてみたらピンヘッダーの金具がないので短くしてもハウジングに付けられない。
ピンヘッダーのパーツAliexpressに売ってる?
仕方がないので長いケーブルそのままに、3Dプリンターで作ったパーツの中にセンサーを収めることにします。
M5Stack Fire プロトケースに収納
M5Stack Fireは磁石でスタッキングするプロトケースの間に3Dプリンターのパーツを挟んで、その中にSGP30センサーを収納します。
ただ↓プロト基盤が邪魔。
結局プロト基盤は邪魔なので取り除いてセンサーやケーブルを収納します。
基盤がなければセンサーをケースに入れても収まる。ケーブルもまとめて入れておけます。
人感センサー
人感センサーはGPIOに接続してケーブルを内蔵、センサーは外に出してLEGO PINで固定しました。
人感センサーの固定はうまくいった。
ケーブルの収まり
ケーブルは一度外にでたものを再度中に入れて収納するようにしました。
これだったらケーブルが長くても中に入って隠れちゃうので、問題なし。
磁石とクリップで3Dプリンターに設置する
3Dプリンターの金属の枠部分にM5Stack Fireを設置するために以下のような磁石を埋め込めるクリップ付きのパーツを作りました。
磁石を入れたパーツとM5Stack Fireとは長めのLEGO PIN↓で合体させる。
M5Stack Fire スタッキング完成

全部合体させると↑このようにスタッキングしすぎで分厚くなりましたが、収まりがよくなったぞ。

うまくいった。これでいい♪
M5Stack Core2のスタッキングパーツ
続いてM5Stack Core2のパーツも3Dプリンターで作っていきます。
Core2のほうは温湿度のENVセンサーを入れるので、厚めのスタッキングパーツが必要↓。
I2Cを3つ刺せるハブ ユニットも入れると↓かなり分厚くなりそう。バッテリーも邪魔なので取り除く。
間に3Dプリンターで作ったパーツを挟み込むので、↓M3の6角ナイロンネジで底面のパーツとジョイントする。M5Stackについていたビスはそのまま使えます。
3Dで作ったパーツをつなげていきます。温湿度センサーは内蔵させたら温度が30度近くになってしまいましたので、ケース内に入れずに外に出すことにしました。
M5Stack Core2は(↓右側)裏にGPIOの穴があるので、ここを利用して温湿度センサーを外にだすことにした。
こんな感じに温湿度センサーを出して固定しました。

M5Stack Core2も完成、ケーブルが収まってスッキリした♪
けど↓やっぱり2・3度温度高め・湿度は低めに計測されます。
ENVもキャリブレーション必要っぽい。
SGP30センサーをスタッキングケースに内蔵できるようにした3Dデータ
3Dデータを元にM5Stack Core2とM5Stack Fire用にそれぞれスタッキングケースを作りました。


3Dプリンターをお持ちの場合はこちら↑を参考にスタッキング試してくださると嬉しいです。
M5Stackで二酸化炭素濃度グラフ完成!
二酸化炭素濃度グラフがちゃんと動くかテスト
「フゥ~っ」と息を吹きかけると↓二酸化炭素が急上昇で、赤い棒グラフになりました。

うまく動いてくれて嬉しい。
結局の所、M5Stackの完成品を作るには別途パーツを作らないとだめで、やはりこれからのモノ作りは3Dプリンター必須になりそう。
SGP30の計測値はあてにならないかも
できたには出来ましたが・・・SGP30はやはりCo2計測には向かないようで、値がぜんぜん違います。↓キャリブレーションしてもあまり意味ないかも。
ただ、12時間ほど経つとなかなかに近い値になってきた。けど、
Xiami二酸化炭素センサーとM5Stack Core2の値は100くらい低いものの、かなり?ちかい値になってきました。
12時間くらい電源を入れておくと、SGP30センサーは安定するようですが、やはりあまりあてにはならなそう・・・
M5Stack Fireの設置場所・3DプリンターのWifi化は便利
SGP30はあまり正確に二酸化炭素濃度を計測できないものの、とりあえず完成したので設置することにしました。
以下のように寝室に置いてある3Dプリンターに設置。
M5Stackとは関係ないのですが、M5Stack Fireの下には3Dプリンターを操作するAstroBoxをインストールしたラズパイでWifi接続してで3Dプリンターを操作しています。

スマホ・タブレット・パソコンのブラウザで3Dプリンターを操作できてかなり便利です。
Astroboxは専用のハードウエアを買う他に、ラズパイにインストールできるOSも配布していますので3Dプリンターのワイヤレス化をしたい方はぜひ試してみてくださいね。
この↑ページの下のほうにダウンロードリンクがあります。↓ZipかTorrentでDL可能。

ラズパイにOSインストールする方法は以下を参考にしてください。

InfluxDBで収集したグラフを見てもSGP30よりMH-Z19のほうが優秀
2・3日常設して電源を抜いていない状態でInfluxDBの収集したデータをGrafanaで表示させてみると、M5Stack FireのSGP30はちゃんと?計測していないみたい・・・
↓下のグラフの寝室とM5Fireは同じ寝室の3Dプリンターのところに置いてあります。計測値がかなり違う!
寝室のグラフはMH-Z19、M5Stack FireはSGP30と、センサーが違うので多少は違ってもいいのですが、ここまで違うと・・・意味なさそうな気がする。

ちょっとは比例しましょうヨ!と言いたい。

ちなみにMH-Z19は↓このようなセンサーです。現在はMH-Z19Cという新バージョンのものも出てきていますので、買う場合はニューバージョンのほうがいいでしょう。
二酸化炭素濃度を計測する場合は、SGP30よりも二酸化炭素センサーなのでこっちのほうが信頼性がありそう。

SGP30はTVOC/eCo2ガスセンサーなので、やはり二酸化炭素濃度計測には向いていないみたい。
M5Stackで電子工作は楽しい
最後にM5Stack FireとCore2のスペックを紹介しておきます。

電子工作が初めてでも最初からいくつかのセンサーが入っていて遊べるので、初心者の方はぜひ試してみて下さいね。
| 画像 | ![]() |
![]() |
|---|---|---|
| 商品名 | M5Stack Core2 | M5Stack FIRE |
| 特徴 | M5Stack Coreの機能改良版 第二世代Core2 | 3つのGROVEポートでさすだけ拡張性の高くLEGO互換パーツでUNITがつなげやすい |
| ブランド | M5Stack | M5Stack |
| 価格 | 約6,000円前後 | 約5,800円前後 |
| MCU | ESP32(D0WDQ6-V3) | ESP32(D0WDQ6-V3) |
| ディスプレイ | 2.0インチ タッチスクリーン(FT6336U) | 2.0インチ320×210 (ILI9342) |
| フラッシュメモリ PSRAM |
16MB / 8MB | 16MB / 4MB |
| GROVEポート | I2C | I2C,I/O,UART,I2C POGOPIN |
| ボタン | 電源,リセット,静電容量 x 3 | 電源,リセット,静電容量 x 3 |
| 機能 | スピーカー(1W-0928),電源LED,振動モーター,RTC(BM8563),PMU(AXP192),MIC(SPM1423),6軸IMU(MPU6886),USB(CP2104) | スピーカー(1W-0928),RGB LEDx10,PMU(AXP192),MIC(BSBE3729),9軸DOF(MPU6886+BMM150),USB(CP2104) |
| MicroSDスロット | 最大16GB | 最大16GB |
| バッテリー | 390mAh(3.7V) | 550mAh(3.7V) |
| サイズ | 54 x 54 x 16mm | 54 x 54 x 20mm |
| 重量 | 52g | 52g |
| このページ 詳細リンク |
詳細を見る | 詳細を見る |

































コメント