ESP8266+CO2モニタ

CO2(二酸化炭素)モニタの作成記録です。部屋に設置して換気の目安にしたり、また新型コロナ感染禍では部屋の人の密度の参考にすることができます。センサーの値をサーバーに残しブラウザでグラフにして表示させることも可能です。

回路

ESP8266の回路は「ESP8266で体温計(ウェアラブルIoT体温計)」と同じものです。

上の回路図ではBME280を入れてありますがCO2センサーSCD30を入れてあります。それとR8とR9のプルアップ抵抗に10Kオームになってますがこのままではパルスが鈍ってi2c通信が出来ません。4.7kオームに変える必要があります。

バッテリー駆動のために昇圧モジュールTLP610986を入れてますが、ACアダプタでの駆動に変更しているので使いません。3.3VのACアダプタかレギュレータで3.3Vを作ってあげます。

基板

基板はKiCADで設計してFusionPCBに発注しました。

二酸化炭素センサー

二酸化炭素センサーはGrove – CO2 & Temperature & Humidity Sensor for Arduino (SCD30) – 3-in-1を使っています。スイッチサイエンスからでも購入できます。

ライブラリはSeeed SCD30 Libraryを使っています。

Arduinoのスケッチ

特に特別な仕掛けはしてません。測定終了後はdeepSleep、wi-fiへの接続失敗の場合にもdeepSleepに入ります。復帰後にはsetupから実行になります。

#include <Wire.h>
#include <ESP8266WiFi.h>
#include "SparkFun_SCD30_Arduino_Library.h"


#define BME_VIN 14
#define LED1 12
#define LED2 13

float result[3] = {0};

SCD30 airSensor;

//wifi
const char* ssid     = "wifiのSSID";
const char* password = "wifiのパスワード";

const char* host = "サーバのドメイン";

int scd30ErrorCount = 0;

void setup() {
  Wire.begin();
  Serial.begin(115200);
  Serial.println("SCD30 Raw Data....");

  pinMode(BME_VIN, OUTPUT);
  digitalWrite(BME_VIN, HIGH);

  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  digitalWrite(LED1, LOW);
  digitalWrite(LED2, LOW);

  SCD30_initialize();

  //wifi setup
  WiFi.begin(ssid, password);

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

  scd30ErrorCount = 0;
}



void loop() {
  float co2;
  float temp;
  float humidity;

  co2 = 0.0;
  temp = 0.0;
  humidity = 0.0;
  
  if (airSensor.dataAvailable()) {
    scd30ErrorCount = 0;

    for (int i = 0 ; i < 3 ; i++) {
      co2 = co2 + airSensor.getCO2();
      temp = temp + airSensor.getTemperature();
      humidity =humidity +  airSensor.getHumidity();
    }
    co2 = co2 /3.0;
    temp = temp / 3.0;
    humidity = humidity/3.0;
    
    
    if (co2 > 0.0) {
      // Use WiFiClient class to create TCP connections
      WiFiClient client;
      const int httpPort = 80;
      if (!client.connect(host, httpPort)) {
        ESP.deepSleep(1 * 5 * 1000 * 1000, WAKE_RF_DEFAULT);//1min = 1min * 5sec * 1000msec * 1000uSec
        delay(1000);//deepsleepモード移行までのダミー命令
      }

      String co2String = String(co2);
      String tempString = String(temp);
      String humidityString = String(humidity);

      String url = "サーバのPHPへのパス/dataUpload.php?c=" + co2String + "&t=" + tempString + "&h=" + humidityString;

      client.println(String("GET ") + url + " HTTP/1.1\r\n" +
                     "Host: " + host + "\r\n" );
      delay(1000);

      while (client.available()) {
        String line = client.readStringUntil('\r');
      }
      ESP.deepSleep(1 * 60 * 1000 * 1000, WAKE_RF_DEFAULT);//1min = 1min * 60sec * 1000msec * 1000uSec
      delay(1000);//deepsleepモード移行までのダミー命令
    }
  } else {
    scd30ErrorCount++;
    if (scd30ErrorCount > 10) {
      ESP.deepSleep(1 * 60 * 1000 * 1000, WAKE_RF_DEFAULT);//1min = 1min * 60sec * 1000msec * 1000uSec
      delay(1000);//deepsleepモード移行までのダミー命令
    }
  }

  delay(2000);

}

サーバーのPHP

ESP8266からwi-fiでデータをサーバ上のPHPに送信しています。PHPは次のように単純なものです。受け取った文字列データをCSV形式で保存しているだけです。phpと同じフォルダにlog.csvで保存します。

<?php

date_default_timezone_set('Asia/Tokyo');//タイムゾーンの設定

//文字コードの設定
mb_internal_encoding("UTF-8");

$temp = $_GET["t"];
$co2 = $_GET["c"];
$humidity = $_GET["h"];


$fhandle = fopen("./log.csv", "a+");//読み込み/書き出し用でオープンします。 ファイルポインタをファイルの終端

//現在時刻の取得とファイルへの書き込み
fwrite($fhandle,date("Y/m/d G:i"));
fwrite($fhandle,",");
fwrite($fhandle,$co2);
fwrite($fhandle,",");
fwrite($fhandle,$temp);
fwrite($fhandle,",");
fwrite($fhandle,$humidity);
fwrite($fhandle, "\n");


//レスポンス
header("Content-type: text/html");
echo "data=";
echo $co2;
echo ",";
echo $temp;
echo ",";
echo $humidity;
echo "C\n";

一応レスポンスを返していますが、ESP8266では受信したレスポンスを利用していません。通信が成功したかどうかを確認したほうが良いですが、面倒なのでやってません。書き込むログファイルは書き込み権限がないと書き込めないのでphpから書き込めるように設定しておきます。

グラフ表示

上記までで基本部分は終わりです。記録を確認するためにwebブラウザ上でグラフ表示します。ログを全て1つのグラフにするととても見にくいグラフになるので24時間ごとに表示するように日付を変えて表示できるようにします。

下図のように日付を入れるフィールドを作ります。webページを開いた時に今日の日付を入れるようにして左右のボタンで日付を変更できるようにしています。下図は2021/5/5の私の部屋のCO2濃度のグラフです。
解説しておくと、この日は早く目が覚めて4時過ぎに部屋に入りました。そこから濃度が上昇。5:30ぐらいに2度寝のために寝室に戻る。そこから濃度が減少。8:30ぐらいに部屋に入り11時過ぎまで作業で濃度が上昇。午後は部屋に入ったり、出たりで濃度も上下しています。

以下のindex.htmlを先程のphpとログファイルが置いてあるディレクトリに置いておきます。

htmlとJavaScriptで書かれています。グラフの描画にはChart.jsを使っています。特に変わったことはしていませんがChart.jsで横軸を時間にした書き方がわかんなくて、そこだけ悩みました。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>ESP8266+SCD30 CO2 log</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/moment.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.1.4/Chart.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <style>
        .graph{
          /*background-color:linen;*/
          pointer-events: none;
        }
        h1{
          font-size: medium;
        }
    </style>

  </head>
  <body onload="setDate()">
    <h1>ESP8266+SCD30 log</h1>
    <form name="date">
      <input type="text" name="year" size="5">
      <input type="text" name="month" size="3">
      <input type="text" name="day" size="3">
      <input type="button" onclick="prevDay();" value="◀︎">
      <!--<input type="button" onclick="sendDateToServerPHP();" value="日付変更">-->
      <input type="button" onclick="nextDay();" value="▶︎">

    </form>
    <div style="width:600px; height:320px;">
    <canvas id="co2Graph" class="graph"></canvas>
    </div>

  <div style="width:600px; height:320px;">
  <canvas id="tempGraph" class="graph"></canvas>
  </div>

  <div style="width:600px; height:320px;">
  <canvas id="humGraph" class="graph"></canvas>
  </div>

  <div style="width:600px; height:320px;">
  <canvas id="fukaiGraph" class="graph"></canvas>
  </div>


  </body>
  <script>
    var dayString;
    var humidityChart;
    var co2Chart;
    var temperatureChart;
    var fukaiChart;

    //ページを開いたときに今日の日付を取得しページのフィールドに表示する。
    //日付をセットてloadDataを呼び出す
    function setDate(){
      var today = new Date();
      document.date.year.value = today.getFullYear();
      document.date.month.value = today.getMonth()+1;
      document.date.day.value = today.getDate();
      loadData();
    }

    //webページに表示されている日付を変数に入れsendDataToServerPHPを呼び出す
    function loadData(){
      var year = document.date.year.value;
      var month = document.date.month.value;
      var day = document.date.day.value;

      sendDateToServerPHP();
      setTimeout(loadData, 1*60*1000);//タイマーで自動でこのページが更新されるようにしている。
    }

    //日付を翌日にする。日付を変更後にsendDateToServerPHPを呼び出す
    function nextDay(){
      var year = document.date.year.value;
      var month = document.date.month.value;
      var day = document.date.day.value;

      day = Number(day) + 1;
      var nD = new Date(year, month-1, day);
      document.date.year.value = nD.getFullYear();
      document.date.month.value = nD.getMonth()+1;
      document.date.day.value = nD.getDate();

      sendDateToServerPHP();
    }

   //日付を前日にする。日付を変更後にsendDateToServerPHPを呼び出す
    function prevDay(){
      var year = document.date.year.value;
      var month = document.date.month.value;
      var day = document.date.day.value;

      day = Number(day) - 1;
      var pD = new Date(year, month-1, day);
      console.log(pD);
      console.log("y=" + year + " m=" + month + " day=" + day);
      document.date.year.value = pD.getFullYear();
      document.date.month.value = pD.getMonth()+1;
      document.date.day.value = pD.getDate();

      sendDateToServerPHP();
    }

   //日付を変数に入れてサーバーのdataDaySelect.phpに日付を送信
   //phpはその日付のcsvデータを送り返してくる
   //request.onloadでデータを受信後の処理を行う。
    function sendDateToServerPHP(){
      var year = document.date.year.value;
      var month = document.date.month.value;
      var day = document.date.day.value;

      console.log("y=" + year + " m=" + month + " d=" + day);
      dayString = year + "/" + month + "/" + day;

      var request = new XMLHttpRequest();
      var URL = "./dataDaySelect.php?year=" + year + "&month=" + month + "&day=" + day;
      //var URL = "dataDaySelect..php";
      request.open('GET', URL, true);
      //request.responseType = 'text';
      request.send(null); // HTTPリクエストの発行

      request.onload = function(){
        convertCSVtoArray(request.responseText);
      }
    }

   //phpからデータを受信後に呼び出される処理
    function convertCSVtoArray(str){ // 読み込んだCSVデータが文字列として渡される
      var result = []; // 最終的な二次元配列を入れるための配列
      var tmp = str.split("\n"); // 改行を区切り文字として行を要素とした配列を生成

      // 各行ごとにカンマで区切った文字列を要素とした二次元配列を生成
      for(var i=0;i<tmp.length;++i){
        result[i] = tmp[i].split(',');
        console.log(tmp[i])
      }

      var temp= [];
      var co2= [];
      var humidity= [];
      var time = [];
      var fukai = [];
      for(var i=0;i<tmp.length;++i){
        time[i] = result[i][0];
        co2[i] = result[i][1];
        temp[i] = result[i][2];
        humidity[i] = result[i][3];

        //不快指数
        fukai[i] = 0.81 * temp[i] + 0.01*humidity[i] * (0.99*temp[i] - 14.3)+46.3;
        //alert(temp[i]);
      }
      for(var i=0;i<temp.length;++i){
        //console.log(temp[i]);
      }

      drawLineGraph(time,co2);//CO2濃度のグラフ描画
      drawLineGraphTemp(time,temp);//気温のグラフ描画
      drawLineGraphHum(time,humidity);//湿度のグラフ描画
      drawLineFukai(time,fukai);//不快指数のグラフ描画
    }

  function drawLineGraphHum(time,humidity){
    var ctx = document.getElementById("humGraph");
    if (humidityChart) {
      humidityChart.destroy();
    }
    humidityChart = new Chart(ctx, {
    type: 'scatter',
    data: {
      labels:time,
      datasets: [
        {
          label: 'humidity[%]',
          data: humidity,
          borderColor: "rgba(255,0,0,1)",
          backgroundColor: "rgba(0,0,0,0)",
          pointRadius: 0.5,
          borderWidth:0.5
        }
      ],
    },
    options: {
      title: {
        display: true,
        text: 'humidity[%]'
      },
      legend: {
        display: false
      },
      tooltips: {
            enabled: false
      },
      animation:false,
      scales: {
        yAxes: [{
          ticks: {
            suggestedMax: 100,
            suggestedMin: 0,
            stepSize: 10,
            //callback: function(value, index, values){
            //  return  value +  '%'
            //}
          }
        }],
        xAxes: [{
          type: "time",
          ticks: {

          },
          time: {
            min: dayString + " 0:00",
            max: dayString + " 23:59",
            unit: "hour",
            displayFormats: {
                hour:  "H"
            }
          }
        }]

      },
    }
  })
  }

  function  drawLineGraphTemp(time,temp){
    var ctx = document.getElementById("tempGraph");
    if (temperatureChart){
      temperatureChart.destroy();
    }
    temperatureChart = new Chart(ctx, {
    type: 'line',
    data: {
      labels:time,
      datasets: [
        {
          label: 'temperature[℃]',
          data: temp,
          borderColor: "rgba(255,0,0,1)",
          backgroundColor: "rgba(0,0,0,0)",
          pointRadius: 0.5,
          borderWidth:0.5
        }
      ],
    },
    options: {
      title: {
        display: true,
        text: 'temperature[℃]'
      },
      legend: {
            display: false
      },
      tooltips: {
            enabled: false
      },
      animation:false,
      scales: {
        yAxes: [{
          ticks: {
            suggestedMax: 40,
            suggestedMin: 0,
            stepSize: 5,
            //callback: function(value, index, values){
             // return  value +  '°C'
            //}
          }
        }],
        xAxes: [{
          type: "time",
          ticks: {

          },
          time: {
            min: dayString + " 0:00",
            max: dayString + " 23:59",
            unit: "hour",
            displayFormats: {
                hour:  "H"
            }
          }
        }]

      },
    }
  })
  }

  function  drawLineGraph(time,co2){
    var ctx = document.getElementById("co2Graph");
    if (co2Chart){
      co2Chart.destroy();
    }
    co2Chart = new Chart(ctx, {
    type: 'line',
    data: {
      labels:time,
      datasets: [
        {
          label: 'CO2[ppm]',
          data: co2,
          borderColor: "rgba(255,0,0,1)",
          backgroundColor: "rgba(0,0,0,0)",
          pointRadius: 0.5,
          borderWidth:0.5
        }
      ],
    },
    options: {
      title: {
        display: true,
        text: 'CO2[ppm]'
      },
      legend: {
            display: false
      },
      tooltips: {
            enabled: false
      },
      animation:false,
      scales: {
        yAxes: [{
          ticks: {
            suggestedMax: 2000,
            suggestedMin: 0,
            stepSize: 500,
            //callback: function(value, index, values){
            //  return  value +  'ppm'
            //}
          }
        }],
        xAxes: [{
          type: "time",
          ticks: {

          },
          time: {
            min: dayString + " 0:00",
            max: dayString + " 23:59",
            unit: "hour",
            displayFormats: {
                hour:  "H"
            }
          }
        }]

      },
    }
  })
  }


  function  drawLineFukai(time,fukai){
    var ctx = document.getElementById("fukaiGraph");
    if (fukaiChart){
      fukaiChart.destroy();
    }
    fukaiChart = new Chart(ctx, {
    type: 'line',
    data: {
      labels:time,
      datasets: [
        {
          label: '不快指数[%]',
          data: fukai,
          borderColor: "rgba(255,0,0,1)",
          backgroundColor: "rgba(0,0,0,0)",
          pointRadius: 0.5,
          borderWidth:0.5
        }
      ],
    },
    options: {
      title: {
        display: true,
        text: '不快指数[%]'
      },
      legend: {
            display: false
      },
      tooltips: {
            enabled: false
      },
      animation:false,
      scales: {
        yAxes: [{
          ticks: {
            suggestedMax: 100,
            suggestedMin: 0,
            stepSize: 10,
            //callback: function(value, index, values){
            //  return  value +  'ppm'
            //}
          }
        }],
        xAxes: [{
          type: "time",
          ticks: {

          },
          time: {
            min: dayString + " 0:00",
            max: dayString + " 23:59",
            unit: "hour",
            displayFormats: {
                hour:  "H"
            }
          }
        }]

      },
    }
  })
  }

  </script>

</html>

ちょっと長いですが、二酸化炭素濃度、温度、湿度、不快指数の4つのグラフを描いてます。

htmlが呼び出されると

body onload="setDate()"

で現在の日付を取得しページ上のフィールドに日付を入力します。次にsetDate()の中からloadData()を呼び出しています。loadDataではフィールドの日付を読み取りその日付のデータのデータをPHPに送信してデータを受け取ります。この仕組みで画面上のボタンで日付を変更してグラフを表示できるようにしています。

日付の変更はnextDay()、prevDay()で行っています。

sendDateToServerPHP()がサーバーのphp、dataDaySelect.phpに日付を送信します。送信後にグラフを描画するようにonloadで以下のようにしています。

request.onload = function(){ 
  convertCSVtoArray(request.responseText); 
}

phpからデータが送られてくるとconvertCSVtoArray()が実行され配列にデータを入れてグラフにします。

日付を受け取ってそのデータを返すphpは次の通りです。設置場所はログやphpと同じディレクトになります。

GETで受け取った年月日を変数に入れます。log.csvのデータを読み込み、日付が一致するデータだけ送り返します。

<?php

date_default_timezone_set('Asia/Tokyo');//タイムゾーンの設定

//文字コードの設定
mb_internal_encoding("UTF-8");

$year = $_GET["year"];
$month = $_GET["month"];
$day = $_GET["day"];

$array = file("./log.csv");//ファイルの中身を配列にいれる。
//$fhandle = fopen("./log.csv", "r");//書き出しでオープン

header("Content-type: text/html");

$i = 0;
$returnString = "";
foreach ($array as $line) {
    $lineArray = explode(',', $line);
    $logDate = $lineArray[0];
    $logCO2 = $lineArray[1];
    $logTemp = $lineArray[2];
    $logHum = $lineArray[3];

    $logHum = str_replace("\n", '', $logHum);

    //echo($logDate . "<br>");
    //echo "<br>";

    $temp = explode(" ", $logDate);//日付と時刻を分解する
    $YMD = $temp[0];
    //echo("YMD=" . $YMD. "<br>");

    $temp2 = explode('/', $YMD);//年月日を分解する
    $temp_year = $temp2[0];
    $temp_month = $temp2[1];
    $temp_day = $temp2[2];

    //echo($temp_year . "/" . $temp_month . "/" . $temp_day . "<br>");
    $HM = $temp[1];


    if (($temp_year == $year) && ($temp_month == $month)&& ($temp_day == $day)){
        //日付が一致したので値を返す
        $returnString = $returnString . $logDate . "," . $logCO2 . "," . $logTemp . "," . $logHum . "\n";
    }

    $i++;
}

//レスポンス

echo $returnString;

サーバーのディレクトにあるファイルは次のようになっています。

dataDaySelect.php
index.html
log.csv
upload.php