EnvProUnitで空気質指数（IAQ）監視、GASで閾値設定してLINENotify送信する。


IoTLTアドベントカレンダー2023　9日目の投稿としての記事です。

### 今回作ったのはこれ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/3835536d-9b5c-fab5-30d2-3a4fa6a8c09c.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/af0e8f90-effd-c512-457b-70ce648839e3.png)

# はじめに

Env Pro Unitは、Bosch社のBME688を搭載し、揮発性有機化合物 (VOC)、揮発性硫黄化合物（VSC）、空気質指数（IAQ）、温度、湿度、気圧などのさまざまな環境パラメータを測定可能な環境センサです。

揮発性有機化合物 (VOC)に反応する代表的なセンサーとして、SENSIRION　SPG30（M5 UnitではTVOC/eCO2センサー）およびSPG40、とBOSCH BME688（M5 UnitではEnv Pro Unit）があります。

揮発性有機化合物 (VOC)に反応するセンサーは、人体に有害な物質を検知して迅速に知らせるシステムを構築するために注目されています。

上記の目的で、SENSIRIONのSPG30、SPG40について詳細に検討している報告があります。12種の有機溶媒Acetone, Acetonitrile, Benzene, Dichloromethane, Diethyl ether, Ethanol, Formic acid, Heptane, Hexane, Isopropanol, Methanol, Toluene　に対する反応を調査しています。
https://www.ncbi.nlm.nih.gov/pmc/articles/PMC8876806/

BOSCH　BME688は、BOSCHのライブラリのサンプルコードでは、非公開のアルゴリズムでgas resistanceからIAQを算出アウトプットしています。この算出アウトプットされたIAQ値は、以下の表のとおりで、IAQ値と空気の汚れが相関しています。化学物質に対する簡易的な評価（コントロールバンディング）ができます。BOSCH BME688データシート内の表は以下の通り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/36a858a3-1ee5-21e9-8294-bc25f582dbff.png)

ミクミンPさんは1つのBME688でいろいろな匂い検出ができるプログラムを公開されています。
https://twitter.com/ksasao/status/1520244490149441536

今回、EnvProUnitのm5stackのサンプルコードを基に、BOSCH　BME688からのIAQ値を記録し、閾値以上になった場合にLINEでお知らせできるようにしました。
今回IAQの閾値を変更する頻度が高いことが予想されるため、スケッチ内に閾値を書き込まず、Google SpreadsheetとGoogle Apps Script内で、閾値を変更できるようにしました。

# 準備
ライブラリーをダウンロードしてインストールした
 * M5Unified: https://github.com/m5stack/M5Unified
 * BME68x Sensor library: https://github.com/boschsensortec/Bosch-BME68x-Library
 * BSEC2 Software Library: https://github.com/boschsensortec/Bosch-BSEC2-Library
 
その後、サンプルプログラム（Arduino）をAtomS3にEnv Pro Unitを接続して動かしてみた。
https://github.com/m5stack/M5Unit-ENV/tree/master/examples/Unit_ENV_PRO

シリアルモニタでtimestamp,iaq,iaq accuracy,temperature,pressure,humidity,gas resistance,stabilization status,run in statusが表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/d3b4bced-2519-4a66-0cf0-2a6f0d718988.png)

空気のよごれの程度を、この空気質指数（IAQ）を指標にして、監視することにする。

# Ambientでグラフ化

サンプルプログラムに、WiFi接続とAmbientに送り込むプログラムを追記して、Ambientでグラフ化した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/4537922c-41f7-0ed9-a479-1ddff3bbc513.png)



# GAS + LINE Notifyでお知らせ
IAQの閾値以上で、空気の質が悪くなったとLINE Notifyに送る機能を追加する。

IAQの閾値を Arduinoスケッチプログラムに入れずに、後に閾値をクラウドで設定変更できるよう、Google Apps Script(GA)を使って作ってみた。

Google Spreadsheetの配置はこれ。2行目のセル(D2)(E2)に閾値を入れて設定する。
5行目以降は1分ごとに送られてくるデータ。144データ残して、それ以上は行を削除して捨てていく。
データはAmbientで保管・グラフ化するので、基本Google Spreadsheetではデータ保管する必要ない。

  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/62de9ef6-3465-f91c-65c6-e79815dcf1f5.png)


### Spreadsheet内のGASを準備

拡張機能 >> Apps Scriptで開く。  
  
LINE_NOTIFY_TOKEN は左欄の歯車マークプロジェクトの設定のスクリプトのプロパティに設定した。

  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/f050e927-8c2b-afcb-878b-766253cf3603.png)

閾値付近をデータが横切って頻繁にLINE Notifyが来るのは避けたいので、以下の方法をとった。
閾値は、threshold_minとthreshold_maxの2種設定する。
直近3回分の最小値がthreshold_maxを超えた場合、かつalert_sent_flag欄がALERTNOTSENTの場合、に「IAQが上がったよ」とお知らせして、alert_sent_flag欄に、ALERTSENTを書き込む。
直近3回分の最大値がthreshold_minを下回った場合、かつalert_sent_flag欄がALERTSENTの場合、に「IAQが下がったよ」とお知らせして、alert_sent_flag欄に、ALERTNOTSENTを書き込む。


<b>Google Spreadsheet内のApps Scriptはこれ。</b>

```javascript

const LINE_NOTIFY_API = 'https://notify-api.line.me/api/notify';
const LINE_NOTIFY_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_NOTIFY_TOKEN');


function doPost(e) {
  //sheetにsheet01を代入します。
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('sheet01');
  //送られてきたデータをJSON形式に変換してpramに代入
  var params = JSON.parse(e.postData.getDataAsString());
  // params内の値を取得する。
  var iaq = params.iaq;
  var iaqaccuracy = params.iaqaccuracy;
  var temperature = params.temperature;
  var pressure = params.pressure;
  var humidity = params.humidity;
  var gasresistance = params.gasresistance;
  var stabilizationstatus = params.stabilizationstatus;
  var runinstatus = params.runinstatus;
  
  // スプレッドシートにM5ATOMS3からの送信データを代入
  //5行目に1行挿入
  sheet.insertRows(5, 1);
  //5行1列目のセル（A5)に日付を書き込む
  sheet.getRange(5, 1).setValue(new Date());
  //5行目に各種データを書き込む
  sheet.getRange(5, 2).setValue(iaq);
  sheet.getRange(5, 3).setValue(iaqaccuracy);
  sheet.getRange(5, 4).setValue(temperature);
  sheet.getRange(5, 5).setValue(pressure);
  sheet.getRange(5, 6).setValue(humidity);
  sheet.getRange(5, 7).setValue(gasresistance);
  sheet.getRange(5, 8).setValue(stabilizationstatus);
  sheet.getRange(5, 9).setValue(runinstatus);

  //3つのセルB5,B6,B7から数値を取ってminとmaxを求めて上段B2,C2に書き込む
  let iaq001 = sheet.getRange(5,2).getDisplayValue();
  let iaq002 = sheet.getRange(6,2).getDisplayValue();
  let iaq003 = sheet.getRange(7,2).getDisplayValue();
  let iaq_min = Math.min(Number(iaq001), Number(iaq002), Number(iaq003));
  let iaq_max = Math.max(Number(iaq001), Number(iaq002), Number(iaq003));
  sheet.getRange(2,2).setValue(iaq_min);
  sheet.getRange(2,3).setValue(iaq_max);

　//thresholdとalertmarkを取得
  let threshold_min = sheet.getRange(2,4).getDisplayValue();
  let threshold_max = sheet.getRange(2,5).getDisplayValue();
  let alertmark = sheet.getRange(2,6).getDisplayValue();


 if((iaq_min > Number(threshold_max)) && (alertmark === 'ALERTNOTSENT')){
    let max_iaq_string = sheet.getRange(2,3).getDisplayValue();
    let message1 = "Index Air Qualityが、" + max_iaq_string + "に上昇しました。"
    lineNotify(message1); 
    sheet.getRange(2,6).setValue('ALERTSENT');
  }

  if((iaq_max < Number(threshold_min)) && (alertmark === 'ALERTSENT')){
    let min_iaq_string = sheet.getRange(2,2).getDisplayValue();
    let message2 = "Index Air Qualityが、" + min_iaq_string + "まで下降しました。"
    lineNotify(message2); 
    sheet.getRange(2,6).setValue('ALERTNOTSENT');
  }

  //144データを残して、それ以降の行は削除する。
  sheet.deleteRow(148);

}



function lineNotify(text) {
  const options = {
      "method"  : "post",
      "payload" : {"message": text},
      "headers" : {"Authorization":"Bearer " + LINE_NOTIFY_TOKEN}
   }
  UrlFetchApp.fetch(LINE_NOTIFY_API, options)
}

```

デプロイして、形式ウェブアプリ、アクセスできるユーザー＝全員としてアクセス承認して取得したURLを以下のArduinoスケッチの該当部分にペーストする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/11166fff-0446-88aa-819f-0219b32ac18d.png)

### AtomS3に書き込むスケッチを準備する。

AtomS3からの測定値はライブラリArduinoJsonを使ってJsonで送る。
ArduinoJsonライブラリを事前にArduinoIDEにインストールしておく。
https://arduinojson.org/?utm_source=meta&utm_medium=library.properties

測定値をグラフにするプログラムはこの記事を参考にさせていただいｔ。
https://qiita.com/YujiSoftware/items/ed29527393d8effa8657

グラフ表示はこれ。
```cpp
 //グラフプロット
    SequencePosition = (SequencePosition + 1) % (sizeof(sequence) / sizeof(int));
    sequence[SequencePosition] = iaq;
    int len = sizeof(sequence) / sizeof(int);
    for (int i = 0; i < len; i++) {
        auto value = sequence[(SequencePosition + 1 + i) % len];
        M5.Lcd.fillCircle(i, (int)(115-value/ymax*100), 1, YELLOW);
    }
```

Y軸の数値の表示はこれ(Tikarawaza)


```cpp
// y軸表示
    M5.Lcd.setTextFont(1);
    M5.Lcd.fillRect(0, 12, 15, 6, BLACK);M5.Lcd.setCursor(0,12,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax));
    M5.Lcd.fillRect(0, 32, 15, 6, BLACK);M5.Lcd.setCursor(0,32,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax/5*4));
    M5.Lcd.fillRect(0, 52, 15, 6, BLACK);M5.Lcd.setCursor(0,52,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax/5*3));
    M5.Lcd.fillRect(0, 72, 15, 6, BLACK);M5.Lcd.setCursor(0,72,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax/5*2));
    M5.Lcd.fillRect(0, 92, 15, 6, BLACK);M5.Lcd.setCursor(0,92,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax/5));
    M5.Lcd.fillRect(0, 112, 15, 6, BLACK);M5.Lcd.setCursor(0,112,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(" 0");
```

中間水平線はこれ(Tikarawaza)

```cpp
//　中間水平線赤
    for(int i=0; i <= 24; i++){
    M5.Lcd.drawPixel(5+5*i, 15, RED);
    M5.Lcd.drawPixel(5+5*i, 35, RED);
    M5.Lcd.drawPixel(5+5*i, 55, RED);
    M5.Lcd.drawPixel(5+5*i, 75, RED);
    M5.Lcd.drawPixel(5+5*i, 95, RED);
    M5.Lcd.drawPixel(5+5*i, 115, RED);
```





IAQは、経時的に波打つので、送信間隔幅（１分幅）での最大値を求める。ambientとGoogle Spreadsheetに送るのはこのIAQの最大値。


```cpp
   //iaqについて送信間隔帯の最大値iaqmaxを求める。
    iaqmax = iaq > iaqmax ? iaq : iaqmax;
```

ArduinoJsonのサイズ計算サイト 
https://arduinojson.org/v6/assistant/#/step1
で計算して
その結果として

```cpp
            StaticJsonDocument<256> doc;
```
の通り<256>にした。

ArduinoJsonを使ってJsonで測定値を送信するスケッチはこれ

```cpp
            //サイズ計算サイト https://arduinojson.org/v6/assistant/#/step1
            StaticJsonDocument<256> doc;
            char buffer[256];
        
            doc["iaq"] = iaqmax;
            doc["iaqaccuracy"] = iaqaccuracy;
            doc["temperature"] = temperature;
            doc["pressure"] = pressure;
            doc["humidity"] = humidity;
            doc["gasresistance"] = gasresistance;
            doc["stabilizationstatus"] = stabilizationstatus;
            doc["runinstatus"] = runinstatus;

            serializeJson(doc, Serial);
            Serial.println("");
            serializeJson(doc, buffer, sizeof(buffer));


            HTTPClient http;
            http.begin(host);
            http.addHeader("Content-Type", "application/json");
            int status_code = http.POST((uint8_t*)buffer, strlen(buffer));
            Serial.printf("status_code=%d\r\n", status_code);
          
            if (status_code == 200) {
            Stream* resp = http.getStreamPtr();

            DynamicJsonDocument json_response(255);
            deserializeJson(json_response, *resp);

            serializeJson(json_response, Serial);
            Serial.println("");
            }
            http.end();
            
```

### 最終のスケッチ


以下のスケッチをAtomS3に書き込む。

```cpp
/**
 * @originalfile Unit_ENV_PRO.ino
 * @originalauthor SeanKwok (shaoxiang@m5stack.com)
 * @originalbrief UNIT ENV PRO TEST DEMO.
 * @originalversion 0.1
 * @originaldate 2023-09-05
 *
 *
 * @Hardwares:UNIT ENV PRO
 * @Platform Version: Arduino M5Stack Board Manager v2.0.7
 * @Dependent Library:
 * M5Unified: https://github.com/m5stack/M5Unified
 * BME68x Sensor library: https://github.com/boschsensortec/Bosch-BME68x-Library
 * BSEC2 Software Library: https://github.com/boschsensortec/Bosch-BSEC2-Library
 */

#include <M5Unified.h>
#include <FastLED.h> 
#include <WiFi.h>  
#include <Ambient.h>
#include <HTTPClient.h>   //https POST
#include <ArduinoJson.h>  //Json送信

#define STATE_SAVE_PERIOD UINT32_C(60 * 60 * 1000)

#define BSEC_MAX_STATE_BLOB_SIZE (221)

#include <EEPROM.h>
static uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE];

// Pls Download BSEC2 Software Library First
#include <bsec2.h>
#include "config/FieldAir_HandSanitizer/FieldAir_HandSanitizer.h"
Bsec2 envSensor;

const char* ssid     = "xxxxxxxxxx";
const char* password = "xxxxxxxxxx";

WiFiClient client;
Ambient ambient;

//Ambient
unsigned int channelID = xxxxx;
const char* writeKey = "xxxxxxxxxx";

int sequence[128] = {};
int SequencePosition = 0;
float ymax =100;

unsigned long previoustime = 0; //60秒毎にデータ送信用
int flashcount = 0; //WiFi接続時の点滅用
int i = 0; //WiFi接続時の点滅用
unsigned long flash_last_time1 = 0;  //5秒毎にGreen点滅用

//google spreadsheet - Google Apps Script
const char* host = "https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec";

float iaq;
float iaqaccuracy;
float temperature;
float pressure;
float humidity;
float gasresistance;
float stabilizationstatus;
float runinstatus;

float iaqmax = 0;

void checkBsecStatus(Bsec2 bsec);

// RGB LEDの数を指定(AtomLiteS3は1, M5Atom Matrixなら25)
#define NUM_LEDS 1
// RGB LEDのDATA PINを指定
#define LED_DATA_PIN 35
// PIR sensor-grove-GPIO AtomS3Lite=1, AtomU=32,
const int digitalPin = 1;

CRGB leds[NUM_LEDS];

bool loadState(Bsec2 bsec) {
    if (EEPROM.read(0) == BSEC_MAX_STATE_BLOB_SIZE) {
        /* Existing state in EEPROM */
        Serial.println("Reading state from EEPROM");
        Serial.print("State file: ");
        for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE; i++) {
            bsecState[i] = EEPROM.read(i + 1);
            Serial.print(String(bsecState[i], HEX) + ", ");
        }
        Serial.println();

        if (!bsec.setState(bsecState)) return false;
    } else {
        /* Erase the EEPROM with zeroes */
        Serial.println("Erasing EEPROM");

        for (uint8_t i = 0; i <= BSEC_MAX_STATE_BLOB_SIZE; i++)
            EEPROM.write(i, 0);

        EEPROM.commit();
    }

    return true;
}

bool saveState(Bsec2 bsec) {
    if (!bsec.getState(bsecState)) return false;

    Serial.println("Writing state to EEPROM");
    Serial.print("State file: ");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE; i++) {
        EEPROM.write(i + 1, bsecState[i]);
        Serial.print(String(bsecState[i], HEX) + ", ");
    }
    Serial.println();

    EEPROM.write(0, BSEC_MAX_STATE_BLOB_SIZE);
    EEPROM.commit();

    return true;
}

void updateBsecState(Bsec2 bsec) {
    static uint16_t stateUpdateCounter = 0;
    bool update                        = false;

    if (!stateUpdateCounter ||
        (stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) {
        /* Update every STATE_SAVE_PERIOD minutes */
        update = true;
        stateUpdateCounter++;
    }

    if (update && !saveState(bsec)) checkBsecStatus(bsec);
}

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

    Serial.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:
                Serial.println("\tiaq = " + String(output.signal));
                Serial.println("\tiaq accuracy = " + String((int)output.accuracy));
                iaq = output.signal;
                iaqaccuracy = int(output.accuracy);
                break;
            case BSEC_OUTPUT_RAW_TEMPERATURE:
                Serial.println("\ttemperature = " + String(output.signal));
                temperature = output.signal;
                break;
            case BSEC_OUTPUT_RAW_PRESSURE:
                Serial.println("\tpressure = " + String(output.signal));
                pressure = output.signal;
                break;
            case BSEC_OUTPUT_RAW_HUMIDITY:
                Serial.println("\thumidity = " + String(output.signal));
                humidity = output.signal;
                break;
            case BSEC_OUTPUT_RAW_GAS:
                Serial.println("\tgas resistance = " + String(output.signal));
                gasresistance = output.signal;
                break;
            case BSEC_OUTPUT_STABILIZATION_STATUS:
                Serial.println("\tstabilization status = " + String(output.signal));
                stabilizationstatus = output.signal;
                break;
            case BSEC_OUTPUT_RUN_IN_STATUS:
                Serial.println("\trun in status = " + String(output.signal));
                runinstatus = output.signal;
                break;
            default:
                break;
        }
    }
    updateBsecState(envSensor);
}

void setup() {
    auto cfg = M5.config();
    M5.begin(cfg);
    M5.In_I2C.release();
    M5.Display.setTextSize(1);
    M5.Lcd.fillRect(0,0,128,128,BLACK); 

    /*
    FastLED.addLeds<WS2811, LED_DATA_PIN, GRB>(leds, NUM_LEDS);  // RGB LEDを初期設定
    FastLED.setBrightness(15);                                   // 明るさを設定（20以上は熱で壊れる可能性あり。）
    leds[0] = CRGB::Red;                                         // LED[0]を赤に設定
    FastLED.show();  
    */

    Serial.begin(115200);

    WiFi.begin(ssid, password);
    int lpcnt=0 ;
    int lpcnt2=0 ;  
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
      lpcnt += 1 ;
      if (lpcnt > 6) {                      // 6回disconnect-connectを試みる
        WiFi.disconnect(true,true);
        WiFi.begin(ssid, password);
        lpcnt = 0 ;
        lpcnt2 += 1 ;
      }
      if (lpcnt2 > 3) {                     // 3回トライで接続できない場合は 
        //leds[0] = CRGB::Red;
        //FastLED.show();
        Serial.println("WiFi is not connected. Now trying to ESPrestart...............");
        delay(3000);
        ESP.restart() ;                      // ソフトウェアリセット
      }
      Serial.print(".");
    }

    /*
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    */

    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());

    EEPROM.begin(BSEC_MAX_STATE_BLOB_SIZE + 1);

      /* Desired subscription list of BSEC2 outputs */
      bsecSensor sensorList[] = {
          BSEC_OUTPUT_IAQ,          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};

    M5.Ex_I2C.begin();

    if (!envSensor.begin(BME68X_I2C_ADDR_HIGH, Wire)) {
          checkBsecStatus(envSensor);
    }

    /* Load the configuration string that stores information on how to
    classify
     * the detected gas */
    if (!envSensor.setConfig(FieldAir_HandSanitizer_config)) {
        checkBsecStatus(envSensor);
    }

    /* Copy state from the EEPROM to the algorithm */
    if (!loadState(envSensor)) {
        checkBsecStatus(envSensor);
    }

    /* Subsribe to the desired BSEC2 outputs */
    if (!envSensor.updateSubscription(sensorList, ARRAY_LEN(sensorList),
                                      BSEC_SAMPLE_RATE_LP)) {
        checkBsecStatus(envSensor);
    }

    /* Whenever new data is available call the newDataCallback function */
    envSensor.attachCallback(newDataCallback);

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

    ambient.begin(channelID, writeKey, &client);
    
    /*
    for (flashcount = 0; flashcount < 5; flashcount++) {  //LEDの点滅start
    leds[0] = CRGB::Blue;
    FastLED.show();
    delay(300);
    leds[0] = CRGB::Black;
    FastLED.show();
    delay(300);
    }  //LEDの点滅end
    */

    delay(100);
    Serial.println("setup finished");
}

void loop(void) {
    auto now = millis();

    /*
    // 定期的に点滅
    if (now - flash_last_time1 >= 5000) {
          leds[0] = CRGB::Green;  // LEDをGreenに設定
          FastLED.show();         // LEDを表示
          delay(100);
          leds[0] = CRGB::Black;  // LEDを黒に設定
          FastLED.show();         // LEDを表示（黒なので消灯）
      flash_last_time1 = now;
    }
    */

    //グラフ表示の開始前に全部消す
    M5.Lcd.fillRect(0, 0, 128, 128, BLACK);


    //newDataCallback内でセンサー値を取得(String 001に入れる)
    if (!envSensor.run()) {
        checkBsecStatus(envSensor);
    }

    M5.Display.setCursor(40, 2); M5.Lcd.setTextFont(1);M5.Lcd.setTextColor(WHITE, BLACK);
    M5.Display.print("IAQ:");
    M5.Lcd.fillRect(35,12,82,32,BLACK); M5.Display.setTextSize(2);M5.Lcd.setTextFont(2);M5.Display.setCursor(35, 12);
    M5.Display.printf("%3d", int(iaq));
    M5.Display.setTextSize(1);


 //シリアルプロッタ表示用
  Serial.print(","); 
  Serial.print(iaq);
  Serial.print(",");
//シリアルプロッタグラフ最大値最小値
  Serial.print(500);
  Serial.print(",");
  Serial.println(0);
  Serial.print(","); 




 // y軸表示
    M5.Lcd.setTextFont(1);
    M5.Lcd.fillRect(0, 12, 15, 6, BLACK);M5.Lcd.setCursor(0,12,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax));
    M5.Lcd.fillRect(0, 32, 15, 6, BLACK);M5.Lcd.setCursor(0,32,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax/5*4));
    M5.Lcd.fillRect(0, 52, 15, 6, BLACK);M5.Lcd.setCursor(0,52,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax/5*3));
    M5.Lcd.fillRect(0, 72, 15, 6, BLACK);M5.Lcd.setCursor(0,72,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax/5*2));
    M5.Lcd.fillRect(0, 92, 15, 6, BLACK);M5.Lcd.setCursor(0,92,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(int(ymax/5));
    M5.Lcd.fillRect(0, 112, 15, 6, BLACK);M5.Lcd.setCursor(0,112,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.println(" 0");

 //　中間水平線赤
    for(int i=0; i <= 24; i++){
    M5.Lcd.drawPixel(5+5*i, 15, RED);
    M5.Lcd.drawPixel(5+5*i, 35, RED);
    M5.Lcd.drawPixel(5+5*i, 55, RED);
    M5.Lcd.drawPixel(5+5*i, 75, RED);
    M5.Lcd.drawPixel(5+5*i, 95, RED);
    M5.Lcd.drawPixel(5+5*i, 115, RED);
    }

 //グラフ横軸の表示
  M5.Lcd.setCursor(95,120,1);M5.Lcd.setTextColor(YELLOW);M5.Lcd.setTextFont(1);M5.Lcd.print("6min"); //loop delayの数字1000-->4min, 2500--> 10min, 5000-->20min, 10000-->40min, 500-->2min

 //グラフプロット
    SequencePosition = (SequencePosition + 1) % (sizeof(sequence) / sizeof(int));
    sequence[SequencePosition] = iaq;
    int len = sizeof(sequence) / sizeof(int);
    for (int i = 0; i < len; i++) {
        auto value = sequence[(SequencePosition + 1 + i) % len];
        //M5.Lcd.drawPixel(i, (int)(115-value/ymax*100), YELLOW);
        M5.Lcd.fillCircle(i, (int)(115-value/ymax*100), 1, YELLOW);
    }

    //iaqについて送信間隔帯の最大値iaqmaxを求める。
    iaqmax = iaq > iaqmax ? iaq : iaqmax;

    if (now - previoustime >= 60000) {//時間(msec)を設定する
        if (WiFi.status() == WL_CONNECTED) {
            ambient.set(1, iaqmax);
            ambient.set(2, iaqaccuracy);
            ambient.set(3, temperature);
            ambient.set(4, pressure);
            ambient.set(5, humidity);
            ambient.set(6, gasresistance);
            ambient.set(7, stabilizationstatus);
            ambient.set(8, runinstatus);
            ambient.send();
            Serial.println("Just Sent To ambiet");       

            delay(500);
            
            //サイズ計算サイト https://arduinojson.org/v6/assistant/#/step1
            StaticJsonDocument<256> doc;
            char buffer[256];
        
            doc["iaq"] = iaqmax;
            doc["iaqaccuracy"] = iaqaccuracy;
            doc["temperature"] = temperature;
            doc["pressure"] = pressure;
            doc["humidity"] = humidity;
            doc["gasresistance"] = gasresistance;
            doc["stabilizationstatus"] = stabilizationstatus;
            doc["runinstatus"] = runinstatus;

            serializeJson(doc, Serial);
            Serial.println("");
            serializeJson(doc, buffer, sizeof(buffer));


            HTTPClient http;
            http.begin(host);
            http.addHeader("Content-Type", "application/json");
            int status_code = http.POST((uint8_t*)buffer, strlen(buffer));
            Serial.printf("status_code=%d\r\n", status_code);
          
            if (status_code == 200) {
            Stream* resp = http.getStreamPtr();

            DynamicJsonDocument json_response(255);
            deserializeJson(json_response, *resp);

            serializeJson(json_response, Serial);
            Serial.println("");
            }
            http.end();
        }
        
          leds[0] = CRGB::Blue;  // LEDをBlueに設定
          FastLED.show();         // LEDを表示
          delay(2000);
          leds[0] = CRGB::Black;  // LEDを黒に設定
          FastLED.show();         // LEDを表示（黒なので消灯）
          
          iaqmax = 0;
          previoustime = now;

    }

//最小値と最大値を求める

float min_value = 10000000;
float max_value = 0;
float total = 0;
float average = 0;

for (int i = 0; i < len ; i++) {
if(sequence[(SequencePosition + 1 + i) % len ] < min_value){
    min_value = sequence[(SequencePosition + 1 + i) % len ];
    }
}
for (int i = 0; i < len ; i++) {
if (sequence[(SequencePosition + 1 + i) % len ] > max_value){
    max_value = sequence[(SequencePosition + 1 + i) % len ];
    }
}
for (int i = 0; i < len ; i++) {
    total += sequence[(SequencePosition + 1 + i) % len ];
}
average =  total/len;


//グラフの縦軸を変更する
    if (max_value < 100){
        ymax = 100;
    }
    else if (max_value < 200){
        ymax = 200;
    }
    else if (max_value < 300){
        ymax = 300;
    }
    else if (max_value < 400){
        ymax = 400;
    }
    else if (max_value <= 500){
        ymax = 500;
    }


    //WiFiが接続していないか確認。接続していない場合は接続を試みる。
    int lpcnt=0 ;
    int lpcnt2=0 ;  
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
      lpcnt += 1 ;
      if (lpcnt > 6) {                      // 6回disconnect-connectを試みる
        WiFi.disconnect(true,true);
        WiFi.begin(ssid, password);
        lpcnt = 0 ;
        lpcnt2 += 1 ;
      }
      if (lpcnt2 > 3) {                     // 3回トライで接続できない場合は 
        leds[0] = CRGB::Red;
        FastLED.show();
        Serial.println("WiFi is not connected. Now trying to ESPrestart...............");
        delay(3000);
        ESP.restart() ;                      // ソフトウェアリセット
      }
      Serial.print(".");
    }

  delay(3000); //この数字でdisplay表示経過時間を制御する　1000-->128sec, 2000--> 256sec, 3000--> 384sec=6min
  M5.update();

}



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

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


# 空気質指数IAQを監視できた。

遠隔でGoogle Spreadsheet内で、LINEで知らせてほしい閾値を設定でき、空気質指数IAQを監視できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/fa65d053-c3d8-512b-275b-4d080d9f59f6.png)



# 目標
BME688が8つ掲載されたBOSCHE BME688 Development kitを使ってみたい。
現在購入してサンプルコードを書き込むときにエラーがでて進まない状態（悲）。
https://www.bosch-sensortec.com/media/boschsensortec/downloads/product_flyer/bst-bme688-fl001.pdf

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269534/7d269b6b-72ff-4342-0b20-58a96dc17ad0.png)