SGP30センサのTVOC(総揮発性有機化合物)&CO2の値をambientで記録してみたらアレ?

前回はM5StickCとSGP30センサでTVOC(総揮発性有機化合物)&CO2が測れるようになりました。このM5StickCをデータの記録可視化ができるambientのサービスを利用して、1日を通して記録して値の変化を見てみたいと思います。

前回の記事はこちら

ambientにデータを送信

ambientのライブラリを利用

以前、はかり式のベッドセンサを作った時のコードを流用します。ambientについて詳細は以下の記事が参考になります。

ambientは、ライブラリがあって、それを使うことでとても簡単にデータを送信することができます。

完成したコード

完成したコードはこちらです。

#include <M5StickC.h>
#include <Wire.h>
#include "Adafruit_SGP30.h"
#include "Adafruit_Sensor.h"
#include "Adafruit_BME280.h"


//ネットワーク関係
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
const char* WifiSSID = "SSID";
const char* WifiPassword = "PASSWORD";

//日時
RTC_TimeTypeDef RTC_TimeStruct;
RTC_DateTypeDef RTC_DateStruct;
const long JST = 3600*9;  //Japan Standard Time GMT+9
struct tm timeInfo;//時刻を格納するオブジェクト
String nowTimeString; //現在時刻のString
unsigned long nowTimeUNIX; //現在時刻のUNIXTime

//Ambient
#include "Ambient.h"
const unsigned int WriteChannelId = XXXXX; // 送信用AmbientのチャネルID
const char* writeKey = "WRITE KEY"; // ライトキー
WiFiClient wifiClient;
Ambient ambient;
const int sendTimeMin = 1; //送信するタイミング[分] 1=1分毎に送信
bool ambientSendOk;  //Ambientへデータが送信できた

//センサー
Adafruit_SGP30 sgp;
Adafruit_BME280 bme;
int counter = 0;  //測定回数カウンタ
double TemperatureSum = 0.0;  //測定値の積算値
double humiditySum = 0.0;
double TVOCMax = 0.0;
double eCO2Max = 0.0;
double TVOCMean = 0.0;
double eCO2Mean = 400.0;

//画面の明るさ
bool lcdOn = false;


void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.setTextFont(4);
  M5.Lcd.setTextColor(TFT_WHITE,TFT_BLACK);  
  M5.Lcd.fillScreen(BLACK);
  
  Serial.begin(115200);
  Serial.println("SGP30 test");

  Wire.begin(0, 26);
  //SGP30 setup
  if (! sgp.begin()){
    Serial.println("Sensor not found :(");
    delay(1000);
    ESP.restart();
    
  }
  Serial.print("Found SGP30 serial #");
  Serial.print(sgp.serialnumber[0], HEX);
  Serial.print(sgp.serialnumber[1], HEX);
  Serial.println(sgp.serialnumber[2], HEX);

  //BME280 seyup
  unsigned status = bme.begin(0x76, &Wire);
  if (!status) {
      Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
      Serial.print("SensorID was: 0x"); Serial.println(bme.sensorID(),16);
      Serial.print("        ID of 0xFF probably means a bad address, a BMP 180 or BMP 085\n");
      Serial.print("   ID of 0x56-0x58 represents a BMP 280,\n");
      Serial.print("        ID of 0x60 represents a BME 280.\n");
      Serial.print("        ID of 0x61 represents a BME 680.\n");
      delay(1000);
      ESP.restart();
  }
  sgp.setIAQBaseline(0x8ED8, 0x8D7A);  // Will vary for each sensor!

  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setRotation(0);
  M5.Lcd.setCursor(0, 0, 2);
  //アクセスポイントに接続
  if( connectToAp() == false )
  {
    //アクセスポイントに接続できなかったらリスタート
    Serial.println("Can't connect. Restert.");
    ESP.restart();
  }

  //RTCをNTPサーバの時間せ設定する
  setRTC();
  getRTCTime();

  //Ambient初期化
  ambient.begin(WriteChannelId, writeKey, &wifiClient); // チャネルIDとライトキーを指定してAmbientの初期化
  
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0, 2);
}

void loop() {
  M5.update();

  //Aボタンを押すと、画面の明るさをON,OFFする
  if( M5.BtnA.pressedFor(1) ){
    if( lcdOn == true )
    {
      lcdOn = false;
      M5.Axp.ScreenBreath( 0 );
    }
    else
    {
      lcdOn = true;
      M5.Axp.ScreenBreath( 15 );
    }

  }

  
  float temperature = bme.readTemperature(); // [°C]
  float humidity = bme.readHumidity(); // [%RH]
  sgp.setHumidity(getAbsoluteHumidity(temperature, humidity));

  delay(100);
  //センサから測定値が得られなければリセット
  if ( !sgp.IAQmeasure() ) {
    Serial.println("Measurement failed");
    for( int i=0 ; i<10 ; i++ )
    {
      if ( sgp.IAQmeasure() )
        break;
      delay(1000);
    }
    if( !sgp.IAQmeasure() )
      ESP.restart();
  }

  //過去との平均をとる 過去4:現在1の比率で平均
  TVOCMean += sgp.TVOC/4.0;
  TVOCMean /= (1+1.0/4.0);
  eCO2Mean += sgp.eCO2/4.0;
  eCO2Mean /= (1+1.0/4.0);

  //測定結果の表示
  int yLocation = 0;
  M5.Lcd.setCursor(0, yLocation, 2);
  M5.Lcd.println("TVOC"); yLocation+=16;
  String str = "      " + (String)(int)round(TVOCMean);
  M5.Lcd.drawRightString(str, M5.Lcd.width(), yLocation,4); yLocation+=24;
  M5.Lcd.drawRightString("[ppb] ", M5.Lcd.width(), yLocation,2); yLocation+=16;
  
  yLocation+=8;
  M5.Lcd.setCursor(0, yLocation , 2);
  M5.Lcd.println("eCO2"); yLocation+=16;
  str = "      " + (String)(int)round(eCO2Mean);
  M5.Lcd.drawRightString(str, M5.Lcd.width(), yLocation,4); yLocation+=24;
  M5.Lcd.drawRightString("[ppm] ", M5.Lcd.width(), yLocation,2); yLocation+=16;

  //指定時間ごとに実行
  M5.Rtc.GetTime(&RTC_TimeStruct);
  if( ( RTC_TimeStruct.Minutes % sendTimeMin ) == 0 && (RTC_TimeStruct.Seconds == 0  ) && counter != 0) 
  {

    Serial.print("Temperature:");Serial.print(TemperatureSum/(double)counter);
    Serial.print("\tHumidity:");Serial.print(humiditySum/(double)counter);
    Serial.print("\tTVOC:"); Serial.print(TVOCMax);
    Serial.print("\teCO2:"); Serial.println(eCO2Max);

    //Ambientへデータを送信
    ambient.set(1, TVOCMax);
    ambient.set(2, eCO2Max);
    ambientSendOk = ambient.send();
    if( ambientSendOk == false )
        Serial.println("ambient send failed.");
        
    TemperatureSum = 0.0;
    humiditySum = 0.0;
    TVOCMax = 0.0;
    eCO2Max = 0.0;

    counter = 0;
  }
  
  TemperatureSum += temperature;
  humiditySum += humidity;
  TVOCMax = max( TVOCMax , TVOCMean );
  eCO2Max = max( eCO2Max , eCO2Mean );
  counter++;

  if (! sgp.IAQmeasureRaw()) {
    Serial.println("Raw Measurement failed");
    delay(1000);
    ESP.restart();
    return;
  }
 
  delay(1000);

}

/* return absolute humidity [mg/m^3] with approximation formula
* @param temperature [°C]
* @param humidity [%RH]
*/
uint32_t getAbsoluteHumidity(float temperature, float humidity) {
    // approximation formula from Sensirion SGP30 Driver Integration chapter 3.15
    const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3]
    const uint32_t absoluteHumidityScaled = static_cast<uint32_t>(1000.0f * absoluteHumidity); // [mg/m^3]
    return absoluteHumidityScaled;
}

//アクセスポイントに接続
bool connectToAp()
{
  Serial.print("Try to connect to AP:");
  Serial.println(WifiSSID);

  //10回接続を試みる
  for( int i=0 ; i<10 ; i++ )
  {
    M5.Lcd.printf("Connect\n" );
    M5.Lcd.printf(" %s\n",WifiSSID);
    Serial.print("Try");
    Serial.print(i+1);
    
    WiFi.mode(WIFI_STA);
    WiFi.disconnect();
    WiFi.begin(WifiSSID, WifiPassword);  //  Wi-Fi APに接続
      
    for( int j=0 ; j<300 ; j++ )
    {
      if( j%10 == 0)
      {
      M5.Lcd.print("*");
      Serial.print("*");        
      }
      delay(100);
      if(WiFi.status() == WL_CONNECTED)
        break;
    }

    if(WiFi.status() == WL_CONNECTED)
      break;

    delay(5000);  //5秒待ってから再接続
  }
  
  Serial.print("WiFi connected\r\nIP address: ");
  Serial.println(WiFi.localIP());
  return (WiFi.status() == WL_CONNECTED);
}

//UNIXTimeをRTCにセットする
void setRTC()
{
  configTime(JST, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");//NTPの設定
  getLocalTime(&timeInfo);//tmオブジェクトのtimeInfoに現在時刻を入れ込む

  char s[30];//文字格納用
  sprintf(s, " %04d/%02d/%02d %02d:%02d:%02d %01%",
          timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
          timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec, timeInfo.tm_wday);//人間が読める形式に変換
  Serial.println(s);//時間をシリアルモニタへ出力
  Serial.println(mktime( &timeInfo ));

  //RTCに日時をセット
  RTC_TimeStruct.Hours   = timeInfo.tm_hour;
  RTC_TimeStruct.Minutes = timeInfo.tm_min;
  RTC_TimeStruct.Seconds = timeInfo.tm_sec;
  M5.Rtc.SetTime(&RTC_TimeStruct);
  RTC_DateStruct.WeekDay = timeInfo.tm_wday;
  RTC_DateStruct.Month = timeInfo.tm_mon + 1;
  RTC_DateStruct.Date = timeInfo.tm_mday;
  RTC_DateStruct.Year = timeInfo.tm_year + 1900;
  M5.Rtc.SetData(&RTC_DateStruct);

  return;
}

//現在時刻のStringとUNIXTimeを設定する
void getRTCTime()
{
  M5.Rtc.GetTime(&RTC_TimeStruct);
  M5.Rtc.GetData(&RTC_DateStruct);

  //Stringを生成
  char s[30]="";
  sprintf(s, "%04d/%02d/%02d-%02d:%02d:%02d",
          RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date,
          RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes, RTC_TimeStruct.Seconds);
  nowTimeString = s;

  //UNIXTimeを生成
  struct tm tmp;
  tmp.tm_year = RTC_DateStruct.Year -1900;
  tmp.tm_mon= RTC_DateStruct.Month -1;
  tmp.tm_mday = RTC_DateStruct.Date;
  tmp.tm_hour = RTC_TimeStruct.Hours;
  tmp.tm_min = RTC_TimeStruct.Minutes;
  tmp.tm_sec = RTC_TimeStruct.Seconds;
  nowTimeUNIX = mktime( &tmp ); 
  //Serial.println(nowTimeUNIX);

  return;
}

はかり式ベッドセンサのコードを流用したので余分な部分もあるかもしれません。NTPサーバから時刻の同期をしていて、毎分0秒のタイミングで、測定値をambientに送信しています。

測定値は、そのままの値(瞬時値)だとばらつきが大きいので、センサからは1秒毎に取得し、移動平均フィルタで滑らかな波形に変換しています。

  TVOCMean += sgp.TVOC/4.0;
  TVOCMean /= (1+1.0/4.0);

このように、過去の値1に対して、最新の値は0.4の比率で移動平均をしています。これによって瞬間的な変化の影響が小さくなり、普通の移動平均よりも波形の変化が滑らかになります。

環境に合わせて変更する部分

このプログラムの中の2箇所、ご自宅の環境に合わせて変更する部分があります。

const char* WifiSSID = "SSID";
const char* WifiPassword = "PASSWORD";

は、ご自宅の無線LANアクセスポイントのSSIDとパスワードを指定します。

const unsigned int WriteChannelId = XXXXX; // 送信用AmbientのチャネルID
const char* writeKey = "WRITE KEY"; // ライトキー

は、ambientのチャネルIDと、ライトキーを指定します。

ambientのライブラリも含めたコード全体は、↓こちら↓からダウンロードできます。

壁に取り付ける

M5StickC用のホルダー

1日中安定して部屋の空気を測定できるように、センサを壁に取り付けます。両面テープで貼ってしまうと、剥がした時にノリが残って汚くなってしまいます。後で取り外すことも考えて、いい感じのホルダーを探してみました。すると、M5StickC用のホルダーの3Dプリンタデータを作られ公開している方がいました。

これを、3Dプリンタで印刷します。

DMMクリエーターズマーケットでも販売されています。3Dプリンタがない方は購入するのもいいでしょう。

スライスと3Dプリント

パラメータはこんな感じでスライスしてみました。サポート速度を上げることで、固くて取り外しにくいサポートではなく、柔らかいフワフワなサポートになります。サポートが外しやすいので速度を速めはオススメです。

フィラメントはPLAが無くなってしまったので、このPTEGフィラメントで印刷しました。

予定では50分程度で印刷が完了します。しばらくは絶好調だったので放置していたら…

盛大にやってしまっています。これこそ3Dプリンですよ。印刷速度が速いのでノズル温度が下がってしまって、フィラメントが適量送られていなくなってしまったようです。ノズルの設定温度を上げて印刷をやり直しました。

綺麗に印刷できました。

サポートを外して、凸凹をちょっとヤスリかけして完成です。綺麗にできました。

3Dプリンタはこちらを使っています。安いけど綺麗に作れていいですよ。

性能を上げる工夫はこちら。結構違うので是非オススメします。

壁に設置

このホルダーを設計した方は、ホチキスで壁に留めていたので真似してみましたが、壁が硬くて針が刺さりませんでした。

そこで、画鋲を使って壁に固定しました。

上からM5SticCを差し込めば設置完了です。

測定結果

しばらく放置してambientの波形を見てみます。

料理すると数値が高く

ある1日の波形です。青がTVOC(目盛は左軸)、赤がeCO2(目盛は右軸)です。

まずわかることは、6:00、12:00、18:00の料理をするタイミングで急激に値が高くなっています。TVOCは朝食料理時に2000[ppb]オーバー、昼食では1500[ppb]、夕食では500[ppb]でした。eCO2は朝食料理時は5000[ppm]オーバー、昼食では5000[ppm]オーバー、夕食では4000[ppm]です。

TCOVは料理をすると2000[ppb]くらいまで上昇してしまうようです。料理によって揮発性の物質が大量に放出されているのかもしれません。

CO2は、屋外が400[ppm]、屋内では1000[ppm]程度が推奨値なので、5000[ppm]というのは異常中の異常です。ちょっとこのCO2の値は信頼していいのか疑問です。SGP30のCO2の値は、CO2を直接測定しているのではなく、エタノールと水素のセンサの測定結果からの推定値となっているようです。

SGP30のデータシートより

このため、TVOCの値の影響を受けてしまっていそうですね。

次の日の波形です。

TVOCとCO2の波形は同じような動きをする傾向がありますね。CO2はTVOCの変化をもっと過敏にした感じの波形になっています。ちょっとこのセンサのCO2の値は信頼性が微妙かもしれません。

今度、CO2センサ専用のセンサの値と、SGP30のCO2の値を比べてみようと思います。

2020.1.29 追加 つづきはこちら

追加終わり

電子工作

関連記事

kohacraftのblog

コメント