ESP32で海面気圧値付きエアモニター

ESP32で海面気圧値付きエアモニター

ESP32で海面気圧値付きエアモニター

以前の記事”ArduinoでCO2を測定”では温度測定にBME280を使用し、同デバイスのサンプルコードをほぼそのまま引用したのでシリアルモニターには気圧と高度が表示されていました。ただし、表示されている気圧は天気図と見合わせると大きく異なりました。

天気図で使われているのは海抜0mの気圧(海面気圧)ですが、私が測定した場所(標高約300m)では気圧が低いので約35hPa低く表示されています。
標高と温度が測れれば海面気圧に換算できる事が分かりました。(シリアルモニターに表示されていた高度は、海面気圧を1013.25hPa(1気圧)として算出されているようです。)

気圧測定値を天気図と見比べるには測定場所の標高が必要

標高は国土地理院やgoogleなどネットかスマホアプリの情報を使うか、GPSデータを使う事になります。

GPSの受信モジュールを探してみるとアマゾンでは500円以下の物もあり安価なのでオフラインで現在地の標高が得られるGPSを使ってみることにしました。

Ambient_AirQualityのスケッチをベースにArduino microで組み始めましたが直ぐにメモリー不足になってしまいましたのでESP32での作成に変更しました。

回路図

スイッチで切り替えてOLEDにGPSの受信データを表示させる予定ですが今回は未着手です。

ブレッドボードで試作

中華の安価なブレッドボードを複数枚使用して組んでみましたが、接触不良の症状が多発し動作が安定せず急遽サンハヤト ニューブレッドボード SAD-01を購入して組みました。

基板でのレイアウト

パーツは交換し易いように全てソケットで実装しました。GPS受信モジュールは2種類購入してみましたので簡単に差し替えられるようにそれぞれ用のソケットを実装しました。

完成



バッテリー駆動もできるようにしましたが、消費電流が約150mAなので比較的容量の大きなモバイルバッテリーでないとGPSやCCS811のデータが安定する前に電池切れになりそうです。

コード
動けばよい程度でサンプルコードにやりたいことを加えていきましたのでダラダラになりました。
HardSerialやWiFi/Ambientは今回は使っていませんが動作を確認した後にコメント化等の処置をしています。
ベースにしたスケッチはスイッチサイエンスのサイト”AmbientでIoTをはじめよう 空気品質を測定し、記録する”です。

/*
 * This sketch is modified from "Ambient_GPS_BME280_CCS811". 
 * Change MCU to ESP32, Added Display SSD1306 OLED.
 */

#include <SoftwareSerial.h>  // No need this line when use Hardware Serial.
#include <WiFi.h>
#include "Ambient.h"

#include <TimeLib.h>
#include <TinyGPS++.h>
#include <Wire.h>
#include "bme280_i2c.h"
#include "SparkFunCCS811.h"
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeMono9pt7b.h>

#define PERIOD 10    // Set measuring interval in sec.

#define Rxpin 16           // No need this line when use Hardware Serial.
#define Txpin 17           // No need this line when use Hardware Serial.
// /HardwareSerial ss(2);  //use HardwareSerial-2 (Pins are fixed: Rx=16 Tx=17)
SoftwareSerial ss;         //use SoftwareSerial

TinyGPSPlus gps;
TinyGPSCustom pdop(gps, "GPGSA", 15); // $GPGSA sentence, 15th element
TinyGPSCustom vdop(gps, "GPGSA", 17); // $GPGSA sentence, 17th element

#define SDA 21
#define SCL 22

#define CCS811_ADDR 0x5A // CCS811 Address
#define CCS811_HW_RESET 5
#define CCS811_WAKE 4
CCS811 ccs811(CCS811_ADDR);

#define SCREEN_WIDTH 128      // OLED display width, in pixels
#define SCREEN_HEIGHT 64      // OLED display height, in pixels
#define SCREEN_ADDRESS 0x3C   // I2C:SSD1306
#define OLED_RESET  -1        // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

BME280 bme280(BME280_I2C_ADDR_PRIM);

const int WFuse = 0;          // 1:WiFi,Ambientを使う  0:使わない

WiFiClient client;
const char* ssid = "xxxx";
const char* password = "xxxxxxxx";
Ambient ambient;
unsigned int channelId = 1234;    // AmbientチャンネルID
const char* writeKey = "xxxxxxxxx"; // Ambientライトキー


const int offset = 9;           // 時刻差(ローカル時刻 - UTC)
int Year;
byte Month, Day, Hour, Minute, Second;
int Lastday = 0;

// CCS811のnRESETピンをLOWにしてリセットする
void ccs811_hw_reset() {
    digitalWrite(CCS811_HW_RESET, LOW);
    delay(10);
    digitalWrite(CCS811_HW_RESET, HIGH);
}

// CCS811のnWAKEピンをLOWにしてI2C通信できるようにする
void ccs811_wake() {
    digitalWrite(CCS811_WAKE, LOW);
    delay(10);
}

// CCS811のnWAKEピンをHIGHにしてI2C通信を止め、消費電力を下げる
void ccs811_sleep() {
    digitalWrite(CCS811_WAKE, HIGH);
}

// /void readCCS811(float humid, float temp);
uint16_t CO2, TVOC;

void setup()
{
    Serial.begin(115200);
    delay(100);
    Serial.println("\r\nAir quality with location");
    // / ss.begin(9600);  //use HardwareSerial2
    ss.begin(9600,SWSERIAL_8N1, Rxpin, Txpin, false); //use SoftwareSerial
    if (!ss) { // If the object did not initialize, then its configuration is invalid
        Serial.println("Invalid SoftwareSerial pin configuration, check config"); 
        while (1) { // Don't continue with invalid configuration
            smartDelay(1000);
        }
    } 
    if (WFuse == 1){
        WiFi.begin(ssid, password);  //  Wi-Fi APに接続
        while (WiFi.status() != WL_CONNECTED) {  //  Wi-Fi AP接続待ち
            Serial.print(".");
            smartDelay(1000);
       }
       Serial.print("WiFi connected\r\nIP address: ");
       Serial.println(WiFi.localIP());
    }

    pinMode(CCS811_HW_RESET, OUTPUT);
    pinMode(CCS811_WAKE, OUTPUT);
    ccs811_hw_reset(); // CCS811のリセット
    ccs811_wake(); // CCS811をI2C通信可能にする

    Wire.begin(SDA, SCL);
    bme280.begin(); // BME280の初期化
    CCS811Core::status returnCode = ccs811.begin(); // CCS811を初期化
    if (returnCode != CCS811Core::SENSOR_SUCCESS) { // 初期化に失敗したら
        Serial.print(".begin() returned with an error: ");
        Serial.println(returnCode, HEX);
        while (1) {
            Serial.print("CCS811 stopiing prog. in setup ");
            display.clearDisplay(); display.setCursor(0, 30); display.print("Err CCS811");
            display.display();
            delay(0); // プログラムを停止する
        }
    }
    ccs811_sleep(); // CCS811のI2C通信を止める

    // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
    if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
       Serial.println(F("SSD1306 allocation failed"));
       for(;;); // Don't proceed, loop forever
    }
    // Show initial display buffer contents on the screen --
    // the library initializes this with an Adafruit splash screen.
    //display.setTextSize(1);                  // Normal 1:1 pixel scale
    display.setTextColor(SSD1306_WHITE);     // Draw white text
    display.clearDisplay(); display.setCursor(0, 30); display.print("in progress");
    display.display();
    smartDelay(1000); // Pause for 1 seconds
    display.setFont(&FreeMono9pt7b);
    
    if (WFuse == 1){
      ambient.begin(channelId, writeKey, &client); // チャネルIDとライトキーを指定してAmbientの初期化
    }
}

void readCCS811(float humid, float temp)
{
    ccs811_wake(); // CCS811をI2C通信可能にする。
    // BME280で読んだ温度、湿度の値を使って補正をおこなう
    ccs811.setEnvironmentalData(humid, temp);
    CO2 = 0;
    while (CO2 < 400 || CO2 > 8192) {       // CCS811の測定範囲は400〜8192ppmなので、範囲外の値は読み飛ばす
        long t = millis();
        while (!ccs811.dataAvailable()) {   // CCS811のデータが読み出し可能かチェック
            smartDelay(100);
            long er = millis() - t;
            Serial.print("waiting for CCS811 data (max:3000 msec) : ");
            Serial.print("Elapsed waiting time is "); Serial.print( er ); Serial.println(" msec.");
            if (er > 3000) {
                // 3秒以上データーが読み出し可能にならなければCCS811をリセットして再起動する
                t = millis();
                Serial.println("data unavailable for too long.");
                ccs811_hw_reset();
                CCS811Core::status returnCode = ccs811.begin(); // CCS811を初期化
                Serial.println("Begin CCS811");
                if (returnCode != CCS811Core::SENSOR_SUCCESS) { // 初期化に失敗したら
                    Serial.print("CCS811.begin() returned with an error: ");
                    Serial.print(returnCode, HEX); Serial.print(":");
                    display.clearDisplay(); display.setCursor(0, 30); display.print("Err CCS811"); display.display();
                    while (1) {
                        delay(0); // プログラムを停止する
                    }
                }
            }
        }
        ccs811.readAlgorithmResults(); // CO2とTVOCの値をCSS811から読み出す。
        CO2 = ccs811.getCO2(); // CO2の値を取得する。
        TVOC = ccs811.getTVOC(); // TVOCの値を取得する。
    }
    ccs811_sleep(); // CCS811のI2C通信を停止する。
}

time_t lasttime = 0;
void loop()
{
    while (ss.available() > 0) {
        if (gps.encode(ss.read())) {
            break;
        }
        // /delay(0);
    }
    
    if ((millis() - lasttime) > PERIOD * 1000 && gps.location.isValid() && gps.altitude.meters()) {
        if (gps.date.day()!= Lastday){
            setTime((gps.time.hour()), (gps.time.minute()), (gps.time.second()), (gps.date.day()), (gps.date.month()), (gps.date.year()));
            adjustTime(offset * SECS_PER_HOUR); // /UTCとJSTの時間差(hour)を秒数に変換してarduinoの時計を調整
            Serial.println("MCU date and time adjusted by GPS ");
            Lastday = gps.date.day();
          }
        Serial.print(year()); Serial.print("/"); Serial.print(month()); Serial.print("/"); Serial.print(day()); Serial.print(" "); 
        Serial.print(hour()); Serial.print(":"); Serial.print(minute()); Serial.print(":"); Serial.print(second()); Serial.print(" "); 
        Serial.print("Elapse: "); Serial.print((millis() - lasttime) / 1000); Serial.print("sec");
        Serial.print(" SATs:"); Serial.println(gps.satellites.value());
        lasttime = millis();
        
        char buf[16];        
        // /緯度・経度はTinyGPS++ライブラリでGPS度分データの分を60で割った10進数の度単位に換算されている
        dtostrf(gps.location.lat(), 10, 6, buf); Serial.print("lat: "); Serial.print(buf);
        dtostrf(gps.location.lng(), 10, 6, buf); Serial.print(", lng: "); Serial.print(buf);
        dtostrf(gps.altitude.meters(), 6, 1, buf); Serial.print(", altitude: "); Serial.print(buf);
        dtostrf(gps.hdop.hdop(), 4, 2, buf); Serial.print(", hdop: "); Serial.print(buf);
        Serial.print(", vdop: "); Serial.print(vdop.value());
        Serial.print(", pdop: "); Serial.print(pdop.value());
        Serial.print("\r\n");

        struct bme280_data data;
        bme280.get_sensor_data(&data);
        dtostrf(data.temperature, 5, 2, buf); Serial.print(", temp: "); Serial.print(buf);
        dtostrf(data.humidity, 5, 2, buf); Serial.print(", humid: "); Serial.print(buf);
        dtostrf(data.pressure / 100, 7, 2, buf); Serial.print(", press: "); Serial.print(buf);
        float press0 = (data.pressure/100) / (pow((1-((0.0065*gps.altitude.meters())/(data.temperature+0.0065*gps.altitude.meters()+273.15))),5.257));
        Serial.print(", press(0m): "); Serial.println(press0);

        // 温度、湿度を渡して、CO2、TVOCの値を測定する
        readCCS811(data.humidity, data.temperature);
        Serial.print("CO2: "); 
        if(CO2 != 400){
           Serial.print(CO2); Serial.print(" ppm,   TVOC: "); Serial.print(TVOC);Serial.println(" ppb");
        }
        else{
          Serial.println("Lower limit");
        }
        Serial.print("\r\n");
     
        //Display
        display.clearDisplay();
        display.setCursor(0, 10);                // Start at top-left corner
        display.print("Temp ");
        display.print(data.temperature, 1);
        display.println("'C");
        display.setCursor(0, 27);
        display.print("RHum ");
        display.print(data.humidity, 1);
        display.println("%");
        display.setCursor(0, 44);
        display.print("Pres ");
        display.print(press0,0);
        display.println("hP");
        display.setCursor(0, 61);
        display.print("CO2 ");
        if(CO2 != 400){
           display.print(CO2);
           display.println("ppm");
        }
        else{
          display.print("Lower");
        }
        display.display();

        if (WFuse == 1){        // 温度、湿度、気圧、CO2の値をAmbientに送信する
           ambient.set(1, data.temperature);
           ambient.set(2, data.humidity);
           ambient.set(3, data.pressure / 100);
           ambient.set(4, press0);
           ambient.set(5, CO2);
           
           dtostrf(gps.location.lat(), 12, 8, buf);
           ambient.set(6, buf);
           dtostrf(gps.location.lng(), 12, 8, buf);
           ambient.set(7, buf);
           ambient.send();
        }
    }
    else{
        if (! gps.location.isValid()){
          Serial.print("\r\nElapse:"); Serial.print(millis()/1000);Serial.println("sec   Waiting for GPS data to stable.");
        }
    }
    smartDelay(1000);
}
static void smartDelay(unsigned long ms){
  // 通常のdelay()を使用してプロセスを止めないように、TinyGPS++がこのsmartDelayを推奨している
  unsigned long start = millis();
  do 
  {
    while (ss.available())
      gps.encode(ss.read());
  } while (millis() - start < ms);
}