M5Stack 二酸化炭素計測グラフ表示とスタッキングケース作成でケーブルスッキリ

M5Stack SGP30SBC(シングルボードコンピュータ)

M5Stackをいじくりまわして遊ぶのは面白いのですが、なかなか使用用途が思いつきません。なにを作るか考えていましたが、結果的にやはり二酸化炭素濃度を表示するのがいちばん良さそう。

作っていて楽しく遊べて、さらに完成したら実用的に二酸化炭素濃度を表示してくれるので、換気の目安にもなって一挙両得な電子工作。

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

それと、M5Stackはコンパクトに収まっていますが、UNITのセンサーをつなげるケーブルが長くて邪魔で不満。

ということでM5Stackの裏にケーブルやセンサーを収めるスタッキングパーツを3Dプリンターで作ってみたので紹介します。

  1. M5StackとSGP30センサーを使った結果・・・
    1. SGP30センサーは二酸化炭素濃度ではあまり使えない・・
    2. M5Stack FireとCore2を使用します
    3. M5Stack FIRE
      1. 3つのGROVEポートでさすだけ拡張性の高くLEGO互換パーツでUNITがつなげやすい
    4. M5Stack Core2
      1. M5Stack Coreの機能改良版 第二世代Core2
  2. M5Stack Co2濃度計測スケッチのもと収集
    1. SGP30
    2. SGP30のキャリブレーション
    3. M5StackのLCD表示
    4. M5Stack ボタン
    5. グラフ
    6. Beep
    7. 温湿度ENVセンサー
    8. データベース(InfluxDB)
  3. M5Stack Fire・Core2 二酸化炭素計測計測 スケッチ
    1. M5Stack Fireに搭載した機能
    2. M5Stack FireのArduino IDEスケッチ
    3. M5Stack Core2に搭載した機能
    4. M5Stack Core2のスケッチ
  4. M5Stack スタッキング用3Dデータ
    1. M5Stack Unitのケーブルがグチャグチャしてイヤ!
    2. M5Stack用スタッキング3Dデータ
    3. LEGO PIN
    4. M5Stack Fireのスタッキングパーツ
      1. M5Stack Fire プロトケースに収納
      2. 人感センサー
      3. ケーブルの収まり
      4. 磁石とクリップで3Dプリンターに設置する
      5. M5Stack Fire スタッキング完成
    5. M5Stack Core2のスタッキングパーツ
    6. SGP30センサーをスタッキングケースに内蔵できるようにした3Dデータ
  5. M5Stackで二酸化炭素濃度グラフ完成!
    1. 二酸化炭素濃度グラフがちゃんと動くかテスト
    2. SGP30の計測値はあてにならないかも
    3. M5Stack Fireの設置場所・3DプリンターのWifi化は便利
    4. InfluxDBで収集したグラフを見てもSGP30よりMH-Z19のほうが優秀
    5. M5Stackで電子工作は楽しい

M5StackとSGP30センサーを使った結果・・・

SGP30センサーは二酸化炭素濃度ではあまり使えない・・

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

⚠ 結論:TVOC/eCo2 SGP30センサーは二酸化炭素濃度の計測には適さない!

これから二酸化炭素濃度をM5Stackで計測する場合は、やはりMH-Z19センサーを使ったほうが良いと思います。

価格は3千円ほどとちょっと高めですが、こちらのセンサーのほうがより正確にCo2を計測できます。

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

M5Stack FireとCore2を使用します

引き続きM5Stack FireとCore2を使用していきます。

M5Stackは最初からケースとディスプレイがあるので、便利ですね。

M5Stack FIRE

M5Stack FIRE
M5Stack

約5,800円前後
MCUESP32(D0WDQ6-V3)
ディスプレイ2.0インチ320×210 (ILI9342)
フラッシュメモリ/PSRAM16MB / 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

M5Stack Core2
M5Stack

約6,000円前後
MCUESP32(D0WDQ6-V3)
ディスプレイ2.0インチ タッチスクリーン(FT6336U)
フラッシュメモリ/PSRAM16MB / 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センサーのユニットについては前回センサーをディスプレイに表示するまではやりましたので、こちらを参考にしてくださいね。

初心者向けM5Stackの使い方 出荷時に簡単に戻す方法とプログラムの探し方+拡張ユニット
M5Stackを買ったはいいけど、まだうまく使えない方は意外と多いようなので、いじくり回し方や工場出荷時に戻す方法を紹介していきます。Githubで公開されているM5Stack用のプログラムも調べたところいくつか実用的なものがありましたので...

SGP30のキャリブレーション

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

M5 CoreInkとEnvHat,TVOC/eCO2 ガスセンサユニット(SGP30)で環境モニタを作る - Qiita
最近某感染症の影響もあって換気が重要になってきたのでCoreInkを使って換気の目安を表示する環境モニタを作ってみましたソースはgithubを参照くださいcoppercele/CoreInkEnvMonitor

ただ、キャリブレーションしてもあまり意味ないの?かなと思っていて、やはり12時間くらい電源を入れっぱなしにしたほうがセンサーが安定するようです。

安定した時に外に出して400PPMくらいの濃度でキャリブレーションすればうまくいくのかな??

M5StackのLCD表示

ディスプレイの表示はこちら↓に詳しく書いてありました。

m5-docs/lcd.md at master · m5stack/m5-docs
TheURLofM5StackOfficialDocuments:.Contributetom5stack/m5-docsdevelopmentbycreatinganaccountonGitHub.

M5Stack ボタン

M5Stackのボタン・Core2のタッチボタンを勉強するためにこちらの記事を参考にさせていただきました。

Fireの物理ボタン・Core2のタッチボタンともに同じスクリプトで動きました。

グラフ

コチラのグラフを参考にM5Stackのグラフ表示を参考にさせていただきました。

GitHub - smoca-ag/m5stack_co2_sensor: Build your own portable, graphing CO₂ sensor
Buildyourownportable,graphingCO₂sensor.Contributetosmoca-ag/m5stack_co2_sensordevelopmentbycreatinganaccountonGitHub.

棒グラフはコチラの方のスケッチを参考にしました。↓

M5Stackで始めるセンサ・インターフェーシング (7) 超音波距離センサを利用 | Arduinoクックブック
IoTspresenseArduinoIDELEDLチカ

Beep

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

M5StackとTVOC/eCO2 ガスセンサユニットで部屋の二酸化炭素濃度をはかり、高かったらBeep音をならして換気を促す - Qiita
伊藤と申します。会社員で、週4回ペースでリモートワークしております。12月半ばになって急に寒くなり、窓を開けると寒いので、なかなか換気ができません。しかし二酸化炭素濃度が上がってくると眠くなるといいますし、空気が悪いと頭痛になっ...

M5Stack Core2には対応していませんでした。現在調査中。

温湿度ENVセンサー

ENVセンサーユニットをM5Stack Core2に搭載していきます。

ライブラリはArduino IDEのツール > ライブラリを管理でBMP280を検索して以下の「Adafruit BMP280 Library」をインストールしました。

ENVセンサー

あと、SHT3Xも使いますのでインストールしておきましょう。

ENVセンサー

M5StackにENVセンサーとSGP30センサーを使う場合は、以下のようにライブラリがフォルダにコピーされますので、ない場合は手動でコピーしましょう。

Arduino IDEで新規ファイルを作ってスクリプトをそのままコピーしてコンパイルするとエラーになります。

ENVセンサー

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をインストールしてスケッチ例をコピー・改造しただけです。

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

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本体はコンパクトで良いのですが、ユニットのセンサーなどのパーツを繋ぐケーブルがかなり長くて収まり悪すぎます・・・

M5Stack

なので、収まりが良くなるように3Dプリンターでスタッキングパーツを作っていきます。

まずは、M5Stack用の3Dデータを物色していきます。

M5Stack用スタッキング3Dデータ

こちらのM5Stackの下にスタッキングできる3Dデータを使わせていただきます。

module shells for M5Stack by n602
moduleshellsforM5StackM5Stack;Thesourceofinformationonhardwareisthefollowingsite.Forthedimensionalinformationnecessaryforthisthing,refertotheinstructionsenclose...

LEGO PIN

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

Lego Technic Pin by VoDkA39
.

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

LEGO Technics Bushing 3 brics by Breunor
Siema

M5Stack Fireのスタッキングパーツ

ほんとうは、ユニットのケーブルを短くしようと思いましたが、よく考えてみたらピンヘッダーの金具がないので短くしてもハウジングに付けられない。

ピンヘッダーのパーツAliexpressに売ってる?

仕方がないので長いケーブルそのままに、3Dプリンターで作ったパーツの中にセンサーを収めることにします。

M5Stack Fire プロトケースに収納

M5Stack Fireは磁石でスタッキングするプロトケースの間に3Dプリンターのパーツを挟んで、その中にSGP30センサーを収納します。

ただ↓プロト基盤が邪魔。

結局プロト基盤は邪魔なので取り除いてセンサーやケーブルを収納します。

M5Stack Fireのスタッキングパーツ

基盤がなければセンサーをケースに入れても収まる。ケーブルもまとめて入れておけます。

人感センサー

人感センサーはGPIOに接続してケーブルを内蔵、センサーは外に出してLEGO PINで固定しました。

M5Stack Fireのスタッキングパーツ

人感センサーの固定はうまくいった。

M5Stack Fireのスタッキングパーツ

ケーブルの収まり

ケーブルは一度外にでたものを再度中に入れて収納するようにしました。

M5Stack Fireのスタッキングパーツ

これだったらケーブルが長くても中に入って隠れちゃうので、問題なし。

M5Stack Fireのスタッキングパーツ

磁石とクリップで3Dプリンターに設置する

3Dプリンターの金属の枠部分にM5Stack Fireを設置するために以下のような磁石を埋め込めるクリップ付きのパーツを作りました。

磁石を入れたパーツとM5Stack Fireとは長めのLEGO PIN↓で合体させる。

M5Stack Fireのスタッキングパーツ

M5Stack Fire スタッキング完成

M5Stack Fireのスタッキングパーツ

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

M5Stack Fireのスタッキングパーツ

うまくいった。これでいい♪

M5Stack Fireのスタッキングパーツ

M5Stack Core2のスタッキングパーツ

続いてM5Stack Core2のパーツも3Dプリンターで作っていきます。

Core2のほうは温湿度のENVセンサーを入れるので、厚めのスタッキングパーツが必要↓。

I2Cを3つ刺せるハブ ユニットも入れると↓かなり分厚くなりそう。バッテリーも邪魔なので取り除く。

M5Stack Core2のスタッキングパーツ

間に3Dプリンターで作ったパーツを挟み込むので、↓M3の6角ナイロンネジで底面のパーツとジョイントする。M5Stackについていたビスはそのまま使えます。

3Dで作ったパーツをつなげていきます。温湿度センサーは内蔵させたら温度が30度近くになってしまいましたので、ケース内に入れずに外に出すことにしました。

M5Stack Core2のスタッキングパーツ

M5Stack Core2は(↓右側)裏にGPIOの穴があるので、ここを利用して温湿度センサーを外にだすことにした。

  M5Stack Core2のスタッキングパーツ

こんな感じに温湿度センサーを出して固定しました。

M5Stack Core2のスタッキングパーツ

M5Stack Core2も完成、ケーブルが収まってスッキリした♪

けど↓やっぱり2・3度温度高め・湿度は低めに計測されます。

ENVもキャリブレーション必要っぽい。

M5Stack Core2のスタッキングパーツ

SGP30センサーをスタッキングケースに内蔵できるようにした3Dデータ

3Dデータを元にM5Stack Core2とM5Stack Fire用にそれぞれスタッキングケースを作りました。

3D design M5Stack-case | Tinkercad
3DdesignM5Stack-casecreatedbyBey.jpwithTinkercad

3Dプリンターをお持ちの場合はこちら↑を参考にスタッキング試してくださると嬉しいです。

M5Stackで二酸化炭素濃度グラフ完成!

二酸化炭素濃度グラフがちゃんと動くかテスト

「フゥ~っ」と息を吹きかけると↓二酸化炭素が急上昇で、赤い棒グラフになりました。

M5Stack  SGP30

うまく動いてくれて嬉しい。

結局の所、M5Stackの完成品を作るには別途パーツを作らないとだめで、やはりこれからのモノ作りは3Dプリンター必須になりそう。

SGP30の計測値はあてにならないかも

できたには出来ましたが・・・SGP30はやはりCo2計測には向かないようで、値がぜんぜん違います。↓キャリブレーションしてもあまり意味ないかも。

ただ、12時間ほど経つとなかなかに近い値になってきた。けど、

M5Stack  SGP30

左側はXiaomi二酸化炭素センサー・中は12時間ほど電源入れっぱなしのM5Stack Core2+SGP30・右側は電源入れたてのM5Stack Fire+SGP30

Xiami二酸化炭素センサーとM5Stack Core2の値は100くらい低いものの、かなり?ちかい値になってきました。

12時間くらい電源を入れておくと、SGP30センサーは安定するようですが、やはりあまりあてにはならなそう・・・

M5Stack Fireの設置場所・3DプリンターのWifi化は便利

SGP30はあまり正確に二酸化炭素濃度を計測できないものの、とりあえず完成したので設置することにしました。

以下のように寝室に置いてある3Dプリンターに設置。

M5Stack

3Dプリンターの枠に磁石でくっつけて設置した。ラズパイからM5Stackの電源を取るとAstroBoxが動かなくなるので、USBアダプタから電源をとった。

M5Stackとは関係ないのですが、M5Stack Fireの下には3Dプリンターを操作するAstroBoxをインストールしたラズパイでWifi接続してで3Dプリンターを操作しています。

Astrobox

スマホ・タブレット・パソコンのブラウザで3Dプリンターを操作できてかなり便利です。

Astroboxは専用のハードウエアを買う他に、ラズパイにインストールできるOSも配布していますので3Dプリンターのワイヤレス化をしたい方はぜひ試してみてくださいね。

Download 3D printer Software | AstroPrint®
Downloadbest3Dprintersoftwareoperatingsystem

この↑ページの下のほうにダウンロードリンクがあります。↓ZipかTorrentでDL可能。

Astrobox

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

ラズパイの簡単インストール方法3種+OSまるごとバックアップ
自宅のIot化でLED照明オンオフ(XiaomiLED)や玄関鍵の開閉(セサミスマートロック)、壁スイッチのオンオフ、部屋の空気の状態を表示させたりしていますが、不満点は専用のアプリでないと操作できないこと。それと同じメーカーでないと他のメ...

InfluxDBで収集したグラフを見てもSGP30よりMH-Z19のほうが優秀

2・3日常設して電源を抜いていない状態でInfluxDBの収集したデータをGrafanaで表示させてみると、M5Stack FireのSGP30はちゃんと?計測していないみたい・・・

↓下のグラフの寝室とM5Fireは同じ寝室の3Dプリンターのところに置いてあります。計測値がかなり違う!

寝室のグラフはMH-Z19、M5Stack FireはSGP30と、センサーが違うので多少は違ってもいいのですが、ここまで違うと・・・意味なさそうな気がする。

M5Stack

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

ちなみにMH-Z19は↓このようなセンサーです。現在はMH-Z19Cという新バージョンのものも出てきていますので、買う場合はニューバージョンのほうがいいでしょう。

二酸化炭素濃度を計測する場合は、SGP30よりも二酸化炭素センサーなのでこっちのほうが信頼性がありそう。

SGP30はTVOC/eCo2ガスセンサーなので、やはり二酸化炭素濃度計測には向いていないみたい。

M5Stackで電子工作は楽しい

最後にM5Stack FireとCore2のスペックを紹介しておきます。

電子工作が初めてでも最初からいくつかのセンサーが入っていて遊べるので、初心者の方はぜひ試してみて下さいね。

画像M5Stack Core2M5Stack FIRE
商品名M5Stack Core2M5Stack FIRE
特徴M5Stack Coreの機能改良版 第二世代Core23つのGROVEポートでさすだけ拡張性の高くLEGO互換パーツでUNITがつなげやすい
ブランドM5StackM5Stack
価格約6,000円前後約5,800円前後
MCUESP32(D0WDQ6-V3)ESP32(D0WDQ6-V3)
ディスプレイ2.0インチ タッチスクリーン(FT6336U)2.0インチ320×210 (ILI9342)
フラッシュメモリ
PSRAM
16MB / 8MB16MB / 4MB
GROVEポートI2CI2C,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 16mm54 x 54 x 20mm
重量52g52g
このページ
詳細リンク
詳細を見る詳細を見る

コメント